Existe uma razão para a atribuição de matriz Swift ser inconsistente (nem uma referência nem uma cópia detalhada)?

216

Estou lendo a documentação e estou constantemente balançando a cabeça com algumas das decisões de design da linguagem. Mas o que realmente me intrigou é como as matrizes são tratadas.

Corri para o parquinho e tentei. Você pode experimentá-los também. Então, o primeiro exemplo:

var a = [1, 2, 3]
var b = a
a[1] = 42
a
b

Aqui ae bsão ambos [1, 42, 3], que eu posso aceitar. As matrizes são referenciadas - OK!

Agora veja este exemplo:

var c = [1, 2, 3]
var d = c
c.append(42)
c
d

cé [1, 2, 3, 42]MAS dé [1, 2, 3]. Ou seja, dviu a mudança no último exemplo, mas não a vê neste. A documentação diz que é porque o comprimento mudou.

Agora, que tal este:

var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e
f

eé [4, 5, 3], o que é legal. É bom ter uma substituição de vários índices, mas fAINDA não vê a alteração, mesmo que o comprimento não tenha sido alterado.

Portanto, para resumir, as referências comuns a uma matriz veem alterações se você alterar 1 elemento, mas se você alterar vários elementos ou anexar itens, é feita uma cópia.

Parece-me um design muito ruim. Estou certo em pensar isso? Existe uma razão pela qual não vejo por que matrizes devem agir assim?

Edição : matrizes foram alteradas e agora têm semântica de valor. Muito mais são!

Cthutu
fonte
95
Para o registro, eu não acho que essa pergunta deva ser encerrada. Swift é um novo idioma, então haverá perguntas como essa por um tempo, como todos aprendemos. Acho esta pergunta muito interessante e espero que alguém tenha um caso convincente na defesa.
Joel Berger
4
@ Joel Fine, pergunte aos programadores, o Stack Overflow é para problemas específicos não programados de programação.
bjb568
21
@ bjb568: Não é opinião, no entanto. Esta pergunta deve ser respondida com fatos. Se algum desenvolvedor Swift vier e responder "Fizemos assim para X, Y e Z", então isso é verdade. Você pode não concordar com X, Y e Z, mas se uma decisão foi tomada para X, Y e Z, isso é apenas um fato histórico do design da linguagem. Bem como quando perguntei por std::shared_ptrque não tem uma versão não atômica, havia uma resposta baseada em fatos, não em opiniões (o fato é que o comitê considerou, mas não o desejou por várias razões).
precisa
7
@ JasonMArcher: Apenas o último parágrafo é baseado na opinião (que sim, talvez deva ser removida). O título real da pergunta (que tomo como a própria pergunta) é responsável por fatos. Não é uma razão as matrizes foram projetados para trabalhar a forma como eles funcionam.
Cornstalks
7
Sim, como o API-Beast disse, isso geralmente é chamado de "Design de linguagem de cópia em meia-boca".
R. Martinho Fernandes

Respostas:

109

Observe que a semântica e a sintaxe da matriz foram alteradas na versão beta 3 do Xcode ( postagem no blog ), portanto, a pergunta não se aplica mais. A resposta a seguir se aplica à versão beta 2:


É por razões de desempenho. Basicamente, eles tentam evitar copiar matrizes o máximo que puderem (e reivindicar "desempenho semelhante a C"). Para citar o livro de idiomas :

Para matrizes, a cópia ocorre apenas quando você executa uma ação com potencial para modificar o comprimento da matriz. Isso inclui anexar, inserir ou remover itens ou usar um índice subscrito à distância para substituir um intervalo de itens na matriz.

Concordo que isso é um pouco confuso, mas pelo menos há uma descrição clara e simples de como funciona.

Essa seção também inclui informações sobre como garantir que uma matriz seja referenciada exclusivamente, como forçar a cópia de matrizes e como verificar se duas matrizes compartilham armazenamento.

Lukas
fonte
61
Acho que você compartilhou e copiou uma GRANDE bandeira vermelha no design.
Cthutu
9
Isto está certo. Um engenheiro me descreveu que, para o design da linguagem, isso não é desejável e é algo que eles esperam "consertar" nas próximas atualizações do Swift. Vote com radares.
Erik Kerber
2
É apenas algo como copiar na gravação (COW) no gerenciamento de memória de processo filho do Linux, certo? Talvez possamos chamá-lo de alteração no tamanho da cópia (COLA). Eu vejo isso como um design positivo.
justhalf
3
@justhalf Posso prever um monte de novatos confusos chegando ao SO e perguntando por que suas matrizes foram / não foram compartilhadas (apenas de uma maneira menos clara).
John Dvorak
11
@justhalf: COW é uma pessimização no mundo moderno de qualquer maneira, e segundo, COW é uma técnica apenas de implementação, e esse material do COLA leva ao compartilhamento e ao compartilhamento não-aleatórios.
Filhote de cachorro
25

A partir da documentação oficial da língua Swift :

Observe que a matriz não é copiada quando você define um novo valor com sintaxe de subscrito, porque definir um valor único com sintaxe de subscrito não tem o potencial de alterar o comprimento da matriz. No entanto, se você anexar um novo item à matriz, modifica o comprimento da matriz . Isso solicita que o Swift crie uma nova cópia da matriz no momento em que você acrescenta o novo valor. A partir de agora, a é uma cópia separada e independente da matriz .....

Leia a seção inteira Comportamento de atribuição e cópia para matrizes nesta documentação. Você descobrirá que, quando substitui um intervalo de itens na matriz, a matriz faz uma cópia de si mesma para todos os itens.

iPatel
fonte
4
Obrigado. Eu me referi a esse texto vagamente na minha pergunta. Mas mostrei um exemplo em que alterar um intervalo de subscritos não mudou o comprimento e ainda era copiado. Portanto, se você não quiser uma cópia, precisará alterá-la um elemento de cada vez.
Cthutu
21

O comportamento mudou com o Xcode 6 beta 3. As matrizes não são mais tipos de referência e possuem um mecanismo de copiar na gravação , ou seja, assim que você altera o conteúdo de uma matriz de uma ou de outra variável, a matriz será copiada e somente o uma cópia será alterada.


Resposta antiga:

Como outros já apontaram, Swift tenta evitar a cópia de matrizes, se possível, inclusive ao alterar valores para índices únicos por vez.

Se você quiser ter certeza de que uma variável de matriz (!) É única, ou seja, não é compartilhada com outra variável, você pode chamar o unsharemétodo Isso copia a matriz, a menos que já tenha apenas uma referência. Obviamente, você também pode chamar o copymétodo, que sempre fará uma cópia, mas o compartilhamento é preferível para garantir que nenhuma outra variável se mantenha na mesma matriz.

var a = [1, 2, 3]
var b = a
b.unshare()
a[1] = 42
a               // [1, 42, 3]
b               // [1, 2, 3]
Pascal
fonte
hmm, para mim, esse unshare()método é indefinido.
precisa saber é o seguinte
1
@ Hlung Foi removido no beta 3, atualizei minha resposta.
Pascal
12

O comportamento é extremamente semelhante ao Array.Resizemétodo no .NET. Para entender o que está acontecendo, pode ser útil examinar o histórico do .token em C, C ++, Java, C # e Swift.

Em C, uma estrutura nada mais é do que uma agregação de variáveis. A aplicação de a .uma variável do tipo de estrutura acessará uma variável armazenada dentro da estrutura. Ponteiros para objetos não contêm agregações de variáveis, mas as identificam . Se alguém tem um ponteiro que identifica uma estrutura, o-> operador poderá ser usado para acessar uma variável armazenada dentro da estrutura identificada pelo ponteiro.

No C ++, estruturas e classes não apenas agregam variáveis, mas também podem anexar código a elas. Usar .para invocar um método em uma variável solicitará que esse método atue sobre o conteúdo da própria variável ; o uso ->de uma variável que identifica um objeto solicitará que o método atue no objeto identificado pela variável.

Em Java, todos os tipos de variáveis ​​personalizadas simplesmente identificam objetos e a invocação de um método em uma variável informa ao método qual objeto é identificado pela variável. As variáveis ​​não podem conter nenhum tipo de tipo de dado composto diretamente, nem existe um meio pelo qual um método possa acessar uma variável na qual é chamado. Essas restrições, embora limitem semanticamente, simplificam bastante o tempo de execução e facilitam a validação de código de código; essas simplificações reduziram a sobrecarga de recursos do Java em um momento em que o mercado era sensível a esses problemas e, portanto, ajudaram a ganhar força no mercado. Eles também significavam que não havia necessidade de um token equivalente ao .usado em C ou C ++. Embora o Java pudesse ter usado,-> da mesma maneira que C e C ++, os criadores optaram por usar caracteres únicos. já que não era necessário para nenhum outro propósito.

Em C # e outras linguagens .NET, as variáveis ​​podem identificar objetos ou reter tipos de dados compostos diretamente. Quando usado em uma variável de um tipo de dados composto, .atua sobre o conteúdo da variável; quando usado em uma variável do tipo de referência, .atua sobre o objeto identificadopor isso. Para alguns tipos de operações, a distinção semântica não é particularmente importante, mas para outros é. As situações mais problemáticas são aquelas em que o método de um tipo de dados composto que modifica a variável na qual é invocado é invocado em uma variável somente leitura. Se for feita uma tentativa de chamar um método em um valor ou variável somente leitura, os compiladores geralmente copiam a variável, deixam o método agir sobre isso e descartam a variável. Isso geralmente é seguro com métodos que apenas leem a variável, mas não é seguro com métodos que gravam nela. Infelizmente, .does ainda não possui meios de indicar quais métodos podem ser usados ​​com segurança com essa substituição e quais não podem.

No Swift, os métodos agregados podem indicar expressamente se eles modificarão a variável sobre a qual são invocados, e o compilador proibirá o uso de métodos de mutação em variáveis ​​somente leitura (em vez de fazer com que elas modifiquem cópias temporárias da variável que, em seguida, ser descartado). Devido a essa distinção, o uso do .token para chamar métodos que modificam as variáveis ​​nas quais são invocadas é muito mais seguro no Swift do que no .NET. Infelizmente, o fato de o mesmo .token ser usado para esse fim e atuar sobre um objeto externo identificado por uma variável significa que a possibilidade de confusão permanece.

Se tivesse uma máquina do tempo e voltasse à criação de C # e / ou Swift, seria possível evitar retroativamente grande parte da confusão em torno desses problemas, fazendo com que os idiomas usassem os tokens .e de ->uma maneira muito mais próxima do uso do C ++. Métodos de agregados e tipos de referência podem ser usados .para atuar sobre a variável sobre a qual foram invocados e ->sobre um valor (para compósitos) ou a coisa identificada por ele (para tipos de referência). Nenhuma linguagem é projetada dessa maneira, no entanto.

Em C #, a prática normal de um método para modificar uma variável na qual é invocado é passar a variável como refparâmetro para um método. Assim, chamar Array.Resize(ref someArray, 23);quando someArrayidentifica uma matriz de 20 elementos fará com someArrayque identifique uma nova matriz de 23 elementos, sem afetar a matriz original. O uso de refdeixa claro que o método deve modificar a variável sobre a qual é invocado. Em muitos casos, é vantajoso poder modificar variáveis ​​sem precisar usar métodos estáticos; Endereços rápidos, ou seja, usando .sintaxe. A desvantagem é que perde esclarecimentos sobre quais métodos agem sobre variáveis ​​e quais métodos agem sobre valores.

supercat
fonte
5

Para mim, isso faz mais sentido se você substituir suas constantes por variáveis:

a[i] = 42            // (1)
e[i..j] = [4, 5]     // (2)

A primeira linha nunca precisa alterar o tamanho de a. Em particular, ele nunca precisa fazer nenhuma alocação de memória. Independentemente do valor de i, esta é uma operação leve. Se você imaginar que sob o capô ahá um ponteiro, ele pode ser um ponteiro constante.

A segunda linha pode ser muito mais complicada. Dependendo dos valores de ie j, pode ser necessário fazer o gerenciamento de memória. Se você imagina que eé um ponteiro que aponta para o conteúdo da matriz, não pode mais assumir que é um ponteiro constante; pode ser necessário alocar um novo bloco de memória, copiar dados do antigo bloco de memória para o novo e alterar o ponteiro.

Parece que os designers de linguagem tentaram manter (1) o mais leve possível. Como (2) pode envolver a cópia de qualquer maneira, eles recorreram à solução de que ela sempre age como se você fizesse uma cópia.

Isso é complicado, mas estou feliz que eles não tenham tornado ainda mais complicado, por exemplo, casos especiais como "se em (2) iej são constantes em tempo de compilação e o compilador pode inferir que o tamanho de e não está indo" para mudar, não copiamos " .


Finalmente, com base no meu entendimento dos princípios de design da linguagem Swift, acho que as regras gerais são as seguintes:

  • Use constantes ( let) sempre em todos os lugares por padrão, e não haverá grandes surpresas.
  • Use variáveis ​​( var) somente se for absolutamente necessário e tenha cuidado com a variação nesses casos, pois haverá surpresas [aqui: cópias implícitas e estranhas de matrizes em algumas situações, mas não em todas].
Jukka Suomela
fonte
5

O que eu descobri é: A matriz será uma cópia mutável da referência, se e somente se a operação tiver o potencial de alterar o comprimento da matriz . No seu último exemplo, f[0..2]indexando com muitos, a operação tem o potencial de alterar seu tamanho (pode ser que duplicatas não sejam permitidas), portanto está sendo copiada.

var e = [1, 2, 3]
var f = e
e[0..2] = [4, 5]
e // 4,5,3
f // 1,2,3


var e1 = [1, 2, 3]
var f1 = e1

e1[0] = 4
e1[1] = 5

e1 //  - 4,5,3
f1 // - 4,5,3
Kumar KL
fonte
8
"tratado como o comprimento mudou" Eu posso entender que seria copiado se o comprimento fosse alterado, mas em combinação com a citação acima, acho que esse é um "recurso" realmente preocupante e que acho que muitas pessoas vão errar
Joel Berger
25
Só porque uma linguagem é nova, não significa que possa conter contradições internas flagrantes.
Lightness Races em órbita
Isso foi "corrigido" na versão beta 3, varagora as matrizes são completamente mutáveis ​​e as letmatrizes são completamente imutáveis.
187 Pascal
4

As strings e matrizes de Delphi tinham exatamente o mesmo "recurso". Quando você olhou para a implementação, fez sentido.

Cada variável é um ponteiro para a memória dinâmica. Essa memória contém uma contagem de referência seguida pelos dados na matriz. Assim, você pode alterar facilmente um valor na matriz sem copiar a matriz inteira ou alterar os ponteiros. Se você deseja redimensionar a matriz, é necessário alocar mais memória. Nesse caso, a variável atual apontará para a memória recém-alocada. Mas você não pode rastrear facilmente todas as outras variáveis ​​que apontam para a matriz original, então as deixa em paz.

Obviamente, não seria difícil fazer uma implementação mais consistente. Se você deseja que todas as variáveis ​​vejam um redimensionamento, faça o seguinte: Cada variável é um ponteiro para um contêiner armazenado na memória dinâmica. O contêiner contém exatamente duas coisas, uma contagem de referência e ponteiro para os dados reais da matriz. Os dados da matriz são armazenados em um bloco separado de memória dinâmica. Agora, há apenas um ponteiro para os dados da matriz, para que você possa redimensioná-lo facilmente e todas as variáveis ​​verão a alteração.

Philip Trade-Ideas
fonte
4

Muitos dos pioneiros do Swift se queixaram dessa semântica de array propensa a erros e Chris Lattner escreveu que a semântica de array foi revisada para fornecer semântica de valor total ( link do desenvolvedor da Apple para quem tem uma conta ). Teremos que esperar pelo menos pela próxima versão beta para ver o que isso significa exatamente.

Gael
fonte
1
O novo comportamento matriz está agora disponível a partir do SDK incluído no iOS 8 / Xcode 6 Beta 3.
smileyborg
0

Eu uso .copy () para isso.

    var a = [1, 2, 3]
    var b = a.copy()
     a[1] = 42 
Preetham
fonte
1
Eu recebo "Valor do tipo '[Int]' tem nenhum membro 'copiar'" quando eu executar o código
jreft56
0

Alguma coisa mudou no comportamento das matrizes nas versões posteriores do Swift? Acabei de executar o seu exemplo:

var a = [1, 2, 3]
var b = a
a[1] = 42
a
b

E meus resultados são [1, 42, 3] e [1, 2, 3]

jreft56
fonte