Estrutura rápida e mutante


96

Há algo que não entendo totalmente quando se trata de alterar os tipos de valor em Swift.

Como afirma o iBook "The Swift Programming Language": Por padrão, as propriedades de um tipo de valor não podem ser modificadas a partir de seus métodos de instância.

E para tornar isso possível, podemos declarar métodos com a mutatingpalavra - chave dentro de structs e enums.

O que não está totalmente claro para mim é o seguinte: você pode alterar uma var de fora de uma estrutura, mas não pode alterá-la a partir de seus próprios métodos. Isso parece contra-intuitivo para mim, já que em linguagens orientadas a objetos, você geralmente tenta encapsular variáveis ​​de forma que elas só possam ser alteradas internamente. Com structs, isso parece ser o contrário. Para elaborar, aqui está um snippet de código:

struct Point {
    var x = 0, y = 0
    mutating func moveToX(x: Int, andY y:Int) { //Needs to be a mutating method in order to work
        self.x = x
        self.y = y
    }
}

var p = Point(x: 1, y: 2)
p.x = 3 //Works from outside the struct!
p.moveToX(5, andY: 5) 

Alguém sabe por que structs não podem alterar seu conteúdo de dentro de seu próprio contexto, enquanto o conteúdo pode ser facilmente alterado em outro lugar?

Respostas:


80

O atributo de mutabilidade é marcado em um armazenamento (constante ou variável), não em um tipo. Você pode pensar que struct tem dois modos: mutável e imutável . Se você atribuir um valor de estrutura a um armazenamento imutável (nós o chamamos letou constante em Swift), o valor se torna o modo imutável e você não pode alterar nenhum estado no valor. (incluindo chamar qualquer método mutante)

Se o valor é atribuído a um armazenamento mutável (chamamos-lhe varou variável em Swift), você está livre para modificar o estado deles, e chamando de método de mutação é permitido.

Além disso, as classes não têm esse modo imutável / mutável. IMO, isso ocorre porque as classes são geralmente usadas para representar entidades referenciadas . E a entidade referenciável geralmente é mutável porque é muito difícil fazer e gerenciar gráficos de referência de entidades de maneira imutável com desempenho adequado. Eles podem adicionar esse recurso mais tarde, mas não agora, pelo menos.

Para programadores Objective-C, os conceitos mutáveis ​​/ imutáveis ​​são muito familiares. Em Objective-C, tínhamos duas classes separadas para cada conceito, mas em Swift, você pode fazer isso com uma estrutura. Meio trabalho.

Para programadores C / C ++, este também é um conceito muito familiar. Isso é exatamente o que as constpalavras-chave fazem em C / C ++.

Além disso, o valor imutável pode ser muito bem otimizado. Em teoria, o compilador Swift (ou LLVM) pode realizar a elisão de cópia em valores passados let, assim como em C ++. Se você usar a estrutura imutável com sabedoria, ela terá melhor desempenho do que as classes contadas novamente.

Atualizar

Como @Joseph afirmou que isso não fornece o motivo , estou adicionando um pouco mais.

As estruturas têm dois tipos de métodos. métodos simples e mutantes . O método simples implica imutável (ou sem mutação) . Essa separação existe apenas para oferecer suporte à semântica imutável . Um objeto em modo imutável não deve mudar seu estado.

Então, os métodos imutáveis ​​devem garantir essa imutabilidade semântica . O que significa que não deve alterar nenhum valor interno. Portanto, o compilador não permite qualquer mudança de estado em um método imutável. Em contraste, os métodos mutantes são livres para modificar estados.

E então, você pode se perguntar por que imutável é o padrão? Isso porque é muito difícil prever o estado futuro de valores mutantes e isso geralmente se torna a principal fonte de dores de cabeça e insetos. Muitas pessoas concordaram que a solução é evitar coisas mutáveis, e então imutável por padrão estava no topo da lista de desejos por décadas nas linguagens da família C / C ++ e suas derivações.

Veja o estilo puramente funcional para mais detalhes. De qualquer forma, ainda precisamos de coisas mutáveis ​​porque coisas imutáveis ​​têm alguns pontos fracos, e discutir sobre elas parece estar fora de assunto.

Eu espero que isso ajude.


2
Então, basicamente, posso interpretá-lo como: Uma estrutura não sabe com antecedência se será mutável ou imutável, portanto, ela assume imutabilidade, a menos que seja dito de outra forma. Visto assim, de fato faz sentido. Isso está correto?
Mike Seghers

1
@MikeSeghers Acho que faz sentido.
eonil

3
Embora isso seja até agora o melhor de duas respostas, não acho que ele responda PORQUE, por padrão, as propriedades de um tipo de valor não podem ser modificadas de dentro de seus métodos de instância. você dá dicas de que é porque o padrão de structs é imutável, mas eu não acho que seja verdade
Joseph Knight

4
@JosephKnight Não é que o padrão de structs seja imutável. É que os métodos de estruturas são padronizados como imutáveis. Pense em C ++ com a suposição inversa. Nos métodos C ++, o padrão é não constante this, você deve adicionar explicitamente consta ao método se quiser que ele tenha um 'const this' e seja permitido em instâncias constantes (métodos normais não podem ser usados ​​se a instância for const). O Swift faz o mesmo com o padrão oposto: um método tem um 'const this' por padrão e você o marca caso contrário, se tiver que ser proibido para instâncias constantes.
Arquivo analógico de

3
@golddove Sim, e você não pode. Você não deveria. Você sempre precisa fazer ou derivar uma nova versão do valor, e seu programa precisa ser projetado para adotar essa forma intencionalmente. O recurso existe para prevenir tal mutação acidental. Novamente, este é um dos conceitos centrais da programação de estilo funcional .
eonil

25

Uma estrutura é uma agregação de campos; se uma instância de estrutura particular for mutável, seus campos serão mutáveis; se uma instância for imutável, seus campos serão imutáveis. Um tipo de estrutura deve, portanto, ser preparado para a possibilidade de que os campos de qualquer instância particular possam ser mutáveis ​​ou imutáveis.

Para que um método de estrutura modifique os campos da estrutura subjacente, esses campos devem ser mutáveis. Se um método que modifica campos da estrutura subjacente é invocado em uma estrutura imutável, ele tentaria modificar campos imutáveis. Uma vez que nada de bom poderia resultar disso, tal invocação precisa ser proibida.

Para conseguir isso, o Swift divide os métodos de estrutura em duas categorias: aqueles que modificam a estrutura subjacente e, portanto, só podem ser invocados em instâncias de estrutura mutável, e aqueles que não modificam a estrutura subjacente e, portanto, devem ser invocáveis ​​em instâncias mutáveis ​​e imutáveis . O último uso é provavelmente o mais frequente e, portanto, o padrão.

Em comparação, o .NET atualmente (ainda!) Não oferece meios de distinguir métodos de estrutura que modificam a estrutura daqueles que não a modificam. Em vez disso, invocar um método de estrutura em uma instância de estrutura imutável fará com que o compilador faça uma cópia mutável da instância de estrutura, deixe o método fazer o que quiser com ela e descartar a cópia quando o método for concluído. Isso tem o efeito de forçar o compilador a perder tempo copiando a estrutura, quer o método a modifique ou não, mesmo que adicionar a operação de cópia quase nunca transforme o que seria código semanticamente incorreto em código semanticamente correto; simplesmente fará com que o código que é semanticamente errado de uma maneira (modificando um valor "imutável") esteja errado de uma maneira diferente (permitindo que o código pense que está modificando uma estrutura, mas descartando as mudanças tentadas). Permitir que os métodos de estrutura indiquem se eles modificarão a estrutura subjacente pode eliminar a necessidade de uma operação de cópia inútil e também garante que a tentativa de uso incorreto seja sinalizada.


1
A comparação com o .NET e um exemplo que não tem a palavra-chave mutating deixou isso claro para mim. O motivo pelo qual a palavra-chave mutante criaria algum benefício.
Binarian

19

Cuidado: termos do leigo à frente.

Esta explicação não é rigorosamente correta no nível de código mais básico. No entanto, foi revisado por um cara que realmente trabalha no Swift e ele disse que é bom o suficiente como uma explicação básica.

Portanto, quero tentar responder de forma simples e direta à pergunta "por que".

Para ser mais preciso: por que temos que marcar as funções de estrutura como mutatingquando podemos alterar os parâmetros de estrutura sem nenhuma palavra-chave de modificação?

Portanto, o quadro geral tem muito a ver com a filosofia que mantém o Swift rápido.

Você pode pensar nisso como o problema de gerenciar endereços físicos reais. Quando você muda seu endereço, se houver muitas pessoas que têm o seu atual, você deve avisar a todas que você se mudou. Mas se ninguém tiver seu endereço atual, você pode simplesmente ir para onde quiser e ninguém precisa saber.

Nessa situação, o Swift é como um correio. Se muitas pessoas com muitos contatos se movimentam muito, a sobrecarga é muito alta. É necessário pagar uma grande equipe para lidar com todas essas notificações, e o processo exige muito tempo e esforço. É por isso que o estado ideal de Swift é que todos em sua cidade tenham o mínimo de contatos possível. Assim, ele não precisa de uma grande equipe para lidar com as alterações de endereço e pode fazer todo o resto de maneira mais rápida e melhor.

É também por isso que o pessoal da Swift está falando a respeito de tipos de valor versus tipos de referência. Por natureza, os tipos de referência acumulam "contatos" em todos os lugares, e os tipos de valor geralmente não precisam de mais do que alguns. Os tipos de valor são "Swift" -er.

Então, de volta a pequena imagem: structs. Structs são muito importantes no Swift porque podem fazer a maioria das coisas que os objetos podem fazer, mas são tipos de valor.

Vamos continuar a analogia do endereço físico imaginando um misterStructque vive em someObjectVille. A analogia fica um pouco complicada aqui, mas acho que ainda é útil.

Portanto, para modelar a alteração de uma variável em a struct, digamos que misterStructtenha cabelo verde e receba uma ordem para mudar para cabelo azul. A analogia fica complicada, como eu disse, mas mais ou menos o que acontece é que em vez de mudar misterStructo cabelo, a pessoa velha se muda e uma nova pessoa com cabelo azul entra, e essa nova pessoa começa a se chamar misterStruct. Ninguém precisa receber uma notificação de mudança de endereço, mas se alguém olhar para esse endereço, verá um cara de cabelo azul.

Agora vamos modelar o que acontece quando você chama uma função em a struct. Nesse caso, é como misterStructobter um pedido como changeYourHairBlue(). Assim, o correio entrega a instrução de misterStruct"mude seu cabelo para azul e diga-me quando terminar".

Se ele está seguindo a mesma rotina de antes, se está fazendo o que fazia quando a variável foi alterada diretamente, o que misterStructvai fazer é sair de sua própria casa e chamar uma nova pessoa de cabelo azul. Mas esse é o problema.

A ordem era "vá mudar seu cabelo para azul e me diga quando terminar", mas foi o cara verde que recebeu o pedido. Depois que o cara azul se muda, uma notificação de "trabalho concluído" ainda precisa ser enviada de volta. Mas o cara azul não sabe nada sobre isso.

[Para realmente entender essa analogia, algo horrível, o que tecnicamente aconteceu com o cara de cabelo verde foi que, depois que ele se mudou, ele imediatamente cometeu suicídio. Portanto, ele também não pode notificar ninguém de que a tarefa está concluída ! ]

Para evitar este problema, em casos como este única , Swift tem que ir diretamente para a casa naquele endereço e realmente mudar o cabelo do habitante atual . Esse é um processo completamente diferente do que apenas enviar um novo cara.

E é por isso que Swift deseja que usemos a mutatingpalavra - chave!

O resultado final parece o mesmo para qualquer coisa que se refira à estrutura: o habitante da casa agora tem cabelo azul. Mas os processos para alcançá-lo são completamente diferentes. Parece que está fazendo a mesma coisa, mas está fazendo algo muito diferente. Está fazendo uma coisa que as estruturas do Swift em geral nunca fazem.

Portanto, para dar ao pobre compilador uma pequena ajuda, e não fazer com que ele descubra se uma função sofre mutação structou não, por conta própria, para cada função de estrutura já existente, somos solicitados a ter pena e usar a mutatingpalavra - chave.

Em essência, para ajudar a Swift a permanecer ágil, todos devemos fazer nossa parte. :)

EDITAR:

Ei cara / dudette que me rebaixou, eu apenas reescrevi completamente minha resposta. Se ficar melhor com você, você removerá o voto negativo?


1
Isso é tão <f-word-here> incrivelmente escrito! Tão fácil de entender. Eu vasculhei a internet para isso e aqui está a resposta (bem, sem ter que passar pelo código-fonte de qualquer maneira). Muito obrigado por dedicar seu tempo para escrevê-lo.
Vaibhav

1
Só quero confirmar que entendi corretamente: podemos dizer que, quando uma propriedade de uma instância do Struct é alterada diretamente no Swift, essa instância do Struct é "encerrada" e uma nova instância com a propriedade alterada está sendo "criada" Por trás das cenas? E quando usamos um método mutante dentro da instância para alterar a propriedade dessa instância, esse processo de encerramento e reinicialização não ocorre?
Ted

@Teo, gostaria de poder responder com certeza, mas o melhor que posso dizer é que passei essa analogia por um desenvolvedor Swift real, e eles disseram "isso é basicamente correto", o que implica que há mais do que isso no sentido técnico. Mas com esse entendimento - que há mais do que apenas isso - em um sentido amplo, sim, é isso que esta analogia está dizendo.
Le Mot Juiced

8

As estruturas Swift podem ser instanciadas como constantes (via let) ou variáveis ​​(via var)

Considere a Arrayestrutura do Swift (sim, é uma estrutura).

var petNames: [String] = ["Ruff", "Garfield", "Nemo"]
petNames.append("Harvey") // ["Ruff", "Garfield", "Nemo", "Harvey"]

let planetNames: [String] = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
planetNames.append("Pluto") //Error, sorry Pluto. No can do

Por que o anexo não funcionou com os nomes dos planetas? Porque append é marcado com a mutatingpalavra - chave. E desde que planetNamesfoi declarado usando let, todos os métodos marcados estão fora dos limites.

Em seu exemplo, o compilador pode dizer que você está modificando a estrutura atribuindo a uma ou mais de suas propriedades fora de um init. Se você alterar seu código um pouco, verá isso xe ynem sempre está acessível fora da estrutura. Observe o letna primeira linha.

let p = Point(x: 1, y: 2)
p.x = 3 //error
p.moveToX(5, andY: 5) //error

5

Considere uma analogia com C ++. Um método struct em Swift sendo mutating/ não- mutatingé análogo a um método em C ++ sendo não- const/ const. Um método marcado constem C ++ de maneira semelhante não pode alterar a estrutura.

Você pode alterar uma var de fora de uma estrutura, mas não pode alterá-la a partir de seus próprios métodos.

Em C ++, você também pode "alterar uma var de fora da estrutura" - mas apenas se tiver uma constvariável não estrutural. Se você tem uma constvariável de estrutura, não pode atribuir a uma var e também não pode chamar um não- constmétodo. Da mesma forma, no Swift, você pode alterar uma propriedade de uma estrutura somente se a variável da estrutura não for uma constante. Se você tiver uma constante de estrutura, não poderá atribuir a uma propriedade e também não poderá chamar um mutatingmétodo.


1

Eu me perguntei a mesma coisa quando comecei a aprender Swift, e cada uma dessas respostas, embora adicionando alguns insights, talvez, são meio prolixas e confusas por direito próprio. Acho que a resposta à sua pergunta é bem simples ...

O método mutante definido dentro de sua estrutura precisa de permissão para modificar cada uma das instâncias de si mesmo que serão criadas no futuro. E se uma dessas instâncias for atribuída a uma constante imutável com let? Uh oh. Para protegê-lo de você mesmo (e para permitir que o editor e o compilador saibam o que você está tentando fazer), você é forçado a ser explícito quando deseja dar a um método de instância esse tipo de poder.

Por outro lado, a configuração de uma propriedade de fora de sua estrutura está operando em uma instância conhecida dessa estrutura. Se for atribuído a uma constante, o Xcode avisará você sobre isso no momento em que você digitar a chamada do método.

Essa é uma das coisas que adoro no Swift à medida que começo a usá-lo mais - ser alertado sobre erros enquanto os digito. Com certeza é melhor do que solucionar bugs obscuros de JavaScript!


1

SWIFT: Uso de função mutante em Structs

Os programadores Swift desenvolveram Structs de tal forma que suas propriedades não podem ser modificadas nos métodos Struct. Por exemplo, verifique o código fornecido abaixo

struct City
{
  var population : Int 
  func changePopulation(newpopulation : Int)
  {
      population = newpopulation //error: cannot modify property "popultion"
  }
}
  var mycity = City(population : 1000)
  mycity.changePopulation(newpopulation : 2000)

Ao executar o código acima, obtemos um erro porque estamos tentando atribuir um novo valor à população de propriedades da Struct City. Por padrão, as propriedades de Structs não podem ser alteradas dentro de seus próprios métodos. É assim que os desenvolvedores da Apple o construíram, de modo que os Structs terão uma natureza estática por padrão.

Como resolvemos isso? Qual é a alternativa?

Palavra-chave mutante:

Declarar função como mutante dentro de Struct nos permite alterar propriedades em Structs. Linha nº: 5, do código acima muda para assim,

mutating changePopulation(newpopulation : Int)

Agora podemos atribuir o valor de newpopulation à população de propriedade dentro do escopo do método.

Nota :

let mycity = City(1000)     
mycity.changePopulation(newpopulation : 2000)   //error: cannot modify property "popultion"

Se usarmos let em vez de var para objetos Struct, então não podemos alterar o valor de nenhuma propriedade. Esta é a razão pela qual obtemos um erro quando tentamos invocar a função mutante usando a instância let. Portanto, é melhor usar var sempre que estiver alterando o valor de uma propriedade.

Adoraria ouvir seus comentários e pensamentos ...

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.