Uma struct do Swift pode compartilhar objetos mutáveis após a atribuição. Para manter as cópias independentes, use propriedades de tipos de valor ou oculte o armazenamento de classe por trás de copy-on-write. Este guia cria uma calculadora sintética de fileiras de ladrilhos para uma prévia na área de trabalho do Mac e verifica se editar uma cópia deixa as outras inalteradas.
Por que copiar a struct ainda altera o original?
Suponha que uma prévia na área de trabalho calcule a largura de uma fileira de ladrilhos. Você copia as configurações para experimentar outra largura de ladrilho, esperando que a prévia original permaneça inalterada. Essa expectativa falha quando ambos os valores de configuração contêm uma referência ao mesmo objeto mutável.
O Swift copia os valores armazenados em uma struct quando ela é atribuída a outra variável. No caso de uma propriedade de classe, esse valor armazenado é uma referência: o objeto referenciado continua compartilhado. O invólucro ainda é uma struct, mas seu comportamento observável pode incluir mutações compartilhadas. Essas regras decorrem da distinção documentada pelo Swift entre tipos de valor e de referência.
Este exemplo completo demonstra o problema. Ele usa apenas a biblioteca padrão do Swift, então não requer importações.
final class TileDimensions {
var width: Int
init(width: Int) {
self.width = width
}
}
struct SharedLayout {
let dimensions: TileDimensions
}
let original = SharedLayout(
dimensions: TileDimensions(width: 40)
)
let edited = original
edited.dimensions.width = 60
assert(original.dimensions === edited.dimensions)
assert(original.dimensions.width == 60)
print(original.dimensions.width) // 60
Declarar a propriedade ou o invólucro com let não congela as propriedades mutáveis do objeto. Isso impede substituir essa referência ou valor de struct. O capítulo sobre estruturas e classes do Swift distingue explicitamente referências constantes de estado imutável do objeto.
Colocar esses objetos em um array também preserva a identidade compartilhada. Cópias de arrays gerenciam seus elementos de forma independente, mas um elemento cujo valor é de classe é, ele próprio, uma referência. Substituir um elemento afeta um array; modificar um objeto referenciado por ambos é observável nos dois. A Apple demonstra essa distinção na documentação de Array.
Qual design dá à calculadora valores independentes?
Defina o contrato antes de escolher uma implementação: após var edited = original, toda edição compatível em qualquer layout deve deixar inalterada a largura calculada do outro layout. Quem lê o tipo não deve precisar saber se o armazenamento é compartilhado internamente.
O guia de decisão a seguir aplica esse contrato a um modelo de prévia local. É uma recomendação de design derivada das regras de cópia documentadas.
| Situação | Design adequado | Condição para preservar a independência |
|---|---|---|
| As configurações consistem em números, strings ou outros componentes com semântica de valor | Armazene esses valores diretamente na struct | Verifique se os componentes aninhados também fornecem valores observáveis independentes |
| É necessário manter o armazenamento existente baseado em classe | Use armazenamento privado com copy-on-write | Toda mutação deve separar primeiro o estado mutável compartilhado |
| É necessário um snapshot separado apenas em uma operação específica | Forneça uma operação explícita de cópia | Documente que a atribuição comum ainda compartilha objetos, a menos que o invólucro também imponha a independência |
| Várias partes do programa editam intencionalmente um objeto | Mantenha a semântica de referência | Descreva na API o comportamento do objeto compartilhado |
Para três configurações inteiras, propriedades diretas na struct são a opção mais simples. A implementação baseada em classe abaixo é útil ao adaptar um design de armazenamento existente ou ao aprender como funciona essa fronteira; ela não estabelece uma vantagem de desempenho para esse pequeno cálculo.
Copy-on-write permite que dois valores compartilhem o armazenamento até que um deles seja alterado. A Apple descreve o encapsulamento de uma classe em uma struct e a aplicação de copy-on-write como uma forma de combinar armazenamento alocado separadamente com semântica de valor em Explore Swift performance. Apenas encapsular a classe não implementa esse comportamento.
Um método copy() explícito, por si só, também deixa uma lacuna: quem chama ainda pode fazer uma atribuição comum sem invocá-lo. Se a atribuição fizer parte do contrato de valor, os caminhos de mutação do invólucro precisam impor a independência automaticamente.
Como implementar e verificar copy-on-write?
Use um único objeto de armazenamento privado, crie-o dentro do invólucro e exponha apenas valores calculados e métodos de mutação controlados. Imediatamente antes de alterar o armazenamento, chame isKnownUniquelyReferenced. Se não for possível confirmar a unicidade, substitua o armazenamento por uma cópia independente. A Apple documenta essa sequência especificamente para implementar copy-on-write.
O cálculo de exemplo usa valores ilustrativos: três ladrilhos, cada um com 40 pontos de largura, e um intervalo de 10 pontos entre ladrilhos adjacentes. Há dois intervalos, então a largura da fileira é de 3 * 40 + 2 * 10 = 140 pontos. Aumentar a largura dos ladrilhos para 60 resulta em 200 pontos.
Para experimentar o programa completo em um Mac, primeiro execute swift --version no Terminal para confirmar que há um toolchain do Swift disponível, conforme recomendado pelo guia Getting Started do Swift. Salve o código a seguir como Layout.swift e execute swift Layout.swift no diretório correspondente. O programa faz os cálculos em memória e imprime os resultados.
struct TileLayout {
private final class Storage {
let columns: Int
var tileWidth: Int
var gap: Int
init(columns: Int, tileWidth: Int, gap: Int) {
self.columns = columns
self.tileWidth = tileWidth
self.gap = gap
}
func copy() -> Storage {
Storage(
columns: columns,
tileWidth: tileWidth,
gap: gap
)
}
}
private var storage: Storage
init(columns: Int, tileWidth: Int, gap: Int) {
precondition((1...1000).contains(columns))
precondition((0...1000).contains(tileWidth))
precondition((0...1000).contains(gap))
storage = Storage(
columns: columns,
tileWidth: tileWidth,
gap: gap
)
}
var rowWidth: Int {
storage.columns * storage.tileWidth
+ (storage.columns - 1) * storage.gap
}
private mutating func ensureUniqueStorage() {
if !isKnownUniquelyReferenced(&storage) {
storage = storage.copy()
}
}
mutating func setTileWidth(_ width: Int) {
precondition((0...1000).contains(width))
ensureUniqueStorage()
storage.tileWidth = width
}
mutating func setGap(_ gap: Int) {
precondition((0...1000).contains(gap))
ensureUniqueStorage()
storage.gap = gap
}
}
var original = TileLayout(columns: 3, tileWidth: 40, gap: 10)
var edited = original
edited.setTileWidth(60)
assert(original.rowWidth == 140)
assert(edited.rowWidth == 200)
original.setGap(5)
assert(original.rowWidth == 130)
assert(edited.rowWidth == 200)
let snapshot = edited
edited.setGap(20)
assert(snapshot.rowWidth == 200)
assert(edited.rowWidth == 220)
print(original.rowWidth, snapshot.rowWidth, edited.rowWidth)
// Expected output: 130 200 220
O programa não requer importações nem pacotes externos. Seus intervalos permitidos são restrições deliberadas do exemplo: de uma a 1.000 colunas e de zero a 1.000 pontos para largura e intervalo. Isso mantém o cálculo inteiro dentro de limites. Um argumento fora do intervalo falha em uma precondição; ele não é ajustado silenciosamente. Se esses métodos setters passarem a aceitar texto editável de uma interface de área de trabalho, valide a entrada antes de chamá-los e ofereça ao usuário uma forma de corrigi-la.
As verificações abrangem três requisitos observáveis: editar o layout copiado preserva o original, editar o original preserva a cópia e manter um snapshot constante preserva seu resultado durante uma edição posterior. Elas testam o comportamento sem exigir uma contagem específica de alocações. O Swift não estava disponível no ambiente de pesquisa, então o exemplo foi inspecionado, mas não executado ali; a saída exibida é o resultado esperado.
O que deve continuar verdadeiro à medida que o invólucro cresce?
A independência do exemplo decorre de toda a fronteira da API. O armazenamento é privado, nenhum inicializador aceita um objeto de armazenamento retido externamente e nenhum getter o retorna. Ambos os métodos de mutação passam pela mesma operação de separação. O getter calcula um resultado sem alterar o armazenamento.
Ao adicionar recursos, examine cada novo caminho que possa alterar o estado observável. Um novo setter, subscrito ou callback de edição precisa do mesmo tratamento que os métodos existentes. Um getter que retorne um objeto filho mutável permitiria que quem chama contornasse a separação, mesmo que o próprio getter fosse somente de leitura.
A cópia do armazenamento também precisa abranger todo o estado mutável relevante para o valor. Aqui, Storage.copy() copia três inteiros, o que basta. Se o armazenamento passar a conter um filho mutável cujo valor seja de classe, atribuir esse filho ao novo armazenamento manterá outra referência compartilhada. Copie esse filho adequadamente ou substitua-o por um componente que já forneça semântica de valor. O comportamento de elementos de classe documentado para Array ilustra a mesma fronteira.
Use esta sequência ao ampliar a calculadora:
- Copie um layout antes de testar cada nova operação de edição.
- Verifique se o layout que não foi editado mantém o resultado anterior.
- Repita com os papéis invertidos.
- Retenha um snapshot, faça várias edições posteriores e verifique-o novamente.
- Inspecione todas as propriedades retornadas e os argumentos de inicializadores em busca de referências mutáveis que possam escapar do invólucro.
Uma classe de armazenamento final impede que subclasses adicionem comportamento, mas não torna suas propriedades imutáveis. Da mesma forma, marcar uma operação como mutating permite que ela atualize a struct; é a lógica de separação que preserva a independência neste exemplo.
Como os objetos do Foundation devem ser copiados?
Se um modelo existente armazena objetos do Foundation, como NSMutableString, use o comportamento de cópia documentado pelo tipo concreto ao implementar a separação do armazenamento. A atribuição no Swift não chama automaticamente os métodos de cópia do Foundation.
O protocolo NSCopying do Foundation define a cópia funcional, enquanto o significado exato depende da classe. A documentação também permite retornar o objeto original quando tanto a classe quanto seu conteúdo são imutáveis. Portanto, a identidade do objeto, por si só, não é um teste universal para determinar se uma cópia atende ao seu contrato de valor.
Quando precisar de um resultado mutável de um tipo com formas mutáveis e imutáveis distintas, consulte NSMutableCopying. A Apple documenta mutableCopy(with:) e o método de conveniência mutableCopy() para essa finalidade. Classes sem essa distinção devem adotar NSCopying.
Para a calculadora de ladrilhos, a fronteira prática é simples: um objeto do Foundation aceito como entrada não pode continuar sendo um alias mutável externo das configurações que deveriam ser independentes. Faça um snapshot independente adequado na entrada, separe corretamente antes das edições e retorne snapshots de valor em vez do objeto mutável interno. Verifique como a classe concreta trata qualquer conteúdo mutável aninhado antes de confiar em sua operação de cópia.
Copy-on-write torna o tipo Sendable?
Independência de valores e conformidade para concorrência exigem análises separadas. A documentação de Sendable da Apple exige que os membros armazenados de uma struct Sendable verificada também sejam Sendable. Uma classe de armazenamento mutável privada não atende a esse requisito apenas porque o tipo que a contém é uma struct.
A verificação de unicidade também não é sincronização. A Apple alerta que isKnownUniquelyReferenced pode retornar true durante acesso simultâneo e exige sincronização apropriada em torno desse uso. Ela verifica referências fortes; ainda podem existir aliases fracos ou não possuídos. Portanto, manter o armazenamento inacessível fora do invólucro é tão importante quanto verificar a unicidade. Consulte os limites documentados da função da biblioteca padrão.
Mantenha este exemplo em um único contexto de cálculo síncrono. Se uma versão posterior precisar passar configurações entre domínios de concorrência, a alternativa direta é uma struct que contenha propriedades de valor Sendable, com conformidade Sendable verificada. Adicionar @unchecked Sendable transfere a responsabilidade pela correção para quem implementa; isso não fornece a coordenação ausente. A Apple explica esses requisitos na referência de Sendable.
Para um pequeno cálculo na área de trabalho, prefira propriedades de valor diretas sempre que atenderem à tarefa. Quando for necessário manter armazenamento de classe mutável, preserve o contrato com propriedade privada, cópias completas do armazenamento, edições controladas e verificações de independência observável. O resultado é previsível: alterar as configurações de trabalho de uma prévia deixa cada valor de layout retido com seu próprio resultado calculado.
Fontes
- Value And Reference Types In Swift — Swift.org.
- Structures and Classes — The Swift Programming Language, projeto Swift.
- Array — Documentação para desenvolvedores da Apple.
- Explore Swift performance — Apple Developer.
- isKnownUniquelyReferenced(_:) — Documentação para desenvolvedores da Apple.
- Getting Started — Swift.org.
- NSCopying — Documentação para desenvolvedores da Apple.
- NSMutableCopying — Documentação para desenvolvedores da Apple.
- Sendable — Documentação para desenvolvedores da Apple.