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 a
e b
sã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, d
viu 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 f
AINDA 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!
std::shared_ptr
que 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).Respostas:
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 :
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.
fonte
A partir da documentação oficial da língua Swift :
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.
fonte
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
unshare
método Isso copia a matriz, a menos que já tenha apenas uma referência. Obviamente, você também pode chamar ocopy
método, que sempre fará uma cópia, mas o compartilhamento é preferível para garantir que nenhuma outra variável se mantenha na mesma matriz.fonte
unshare()
método é indefinido.O comportamento é extremamente semelhante ao
Array.Resize
mé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
ref
parâmetro para um método. Assim, chamarArray.Resize(ref someArray, 23);
quandosomeArray
identifica uma matriz de 20 elementos fará comsomeArray
que identifique uma nova matriz de 23 elementos, sem afetar a matriz original. O uso deref
deixa 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.fonte
Para mim, isso faz mais sentido se você substituir suas constantes por variáveis:
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 dei
, esta é uma operação leve. Se você imaginar que sob o capôa
há um ponteiro, ele pode ser um ponteiro constante.A segunda linha pode ser muito mais complicada. Dependendo dos valores de
i
ej
, pode ser necessário fazer o gerenciamento de memória. Se você imagina quee
é 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:
let
) sempre em todos os lugares por padrão, e não haverá grandes surpresas.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].fonte
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.fonte
var
agora as matrizes são completamente mutáveis e aslet
matrizes são completamente imutáveis.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.
fonte
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.
fonte
Eu uso .copy () para isso.
fonte
Alguma coisa mudou no comportamento das matrizes nas versões posteriores do Swift? Acabei de executar o seu exemplo:
E meus resultados são [1, 42, 3] e [1, 2, 3]
fonte