Mantenha estáveis as propriedades usadas pela igualdade e pela função hash enquanto um objeto for uma chave de dicionário. Para um cálculo local em Swift no Mac, escolha identidade do objeto, um ID imutável ou um instantâneo de valores, conforme o significado desejado para a consulta.

Este passo a passo usa retângulos sintéticos e pequenos cálculos de área mantidos inteiramente na memória. A tarefa prática é permitir que o rótulo ou as dimensões de um retângulo mudem sem perder a entrada de dicionário associada — e sem confundir uma consulta bem-sucedida com um cálculo atualizado.

Por que mudar um objeto pode interromper uma consulta ao dicionário?

O Dictionary do Swift é uma tabela hash cujas chaves estão em conformidade com Hashable. O subscrito baseado em chave retorna um opcional porque a chave solicitada pode estar ausente. O hash ajuda a localizar uma entrada; a igualdade determina quais chaves são consideradas equivalentes. Consulte a documentação de Dictionary da Apple.

O contrato de Hashable conecta essas operações: instâncias iguais devem fornecer os mesmos componentes essenciais ao Hasher, na mesma ordem. Ao implementar uma chave personalizada, use as propriedades que definem a igualdade em hash(into:). A Apple descreve esse requisito em Hashable.

Uma chave de tipo de referência traz outra consideração. Atribuir uma instância de classe passa uma referência ao objeto existente, em vez de criar um objeto independente. Declarar a referência com let não congela suas propriedades mutáveis. Essas regras da linguagem estão documentadas em Structures and Classes.

Suponha que uma classe de retângulo compare e calcule o hash do seu rótulo editável. Você insere o objeto quando o rótulo é "Tile A" e, em seguida, muda esse rótulo para "Tile B". A referência armazenada agora expõe um estado diferente para a definição da chave. A posição original no dicionário e a consulta posterior já não têm uma correspondência estável.

A regra de projeto decorre dos contratos da tabela hash e da igualdade: mantenha estável o estado que define a igualdade da chave enquanto ela estiver no dicionário. A Apple declara explicitamente o requisito correspondente para objetos Foundation: o hash de um objeto não deve mudar enquanto ele pertencer a uma coleção que use esse hash para posicioná-lo. Consulte NSObjectProtocol.hash.

Não crie um teste supondo que toda mutação inválida retorne imediatamente nil. Depois que o contrato da chave é violado, uma consulta aparentemente bem-sucedida não basta para provar que o projeto funciona. Em vez disso, teste as regras de igualdade desejadas e o comportamento estável.

Qual chave corresponde à tarefa?

Escolha o significado de uma entrada de dicionário antes de implementar a conformidade. Para uma ferramenta de cálculo da área de retângulos, estas três perguntas levam a projetos diferentes:

O que uma entrada deve representar? Chave adequada O que acontece quando o retângulo muda?
Este objeto retângulo específico Chave de classe baseada na identidade A entrada continua associada a essa instância
Um retângulo lógico identificado independentemente do objeto atual Valor de ID imutável A entrada continua associada ao ID
Um cálculo para dimensões específicas Instantâneo imutável das dimensões Dimensões alteradas geram uma chave de consulta diferente

Esta tabela serve como auxílio de projeto, derivado das regras de identidade, valores e hashing do Swift. Ela também separa duas obrigações: manter uma associação acessível e manter atualizado um resultado calculado. Uma chave de identidade estável resolve a primeira; recalcular ou usar um instantâneo completo das entradas resolve a segunda.

Use a identidade do objeto quando a entrada pertencer a uma instância

Para uma associação em memória que deve acompanhar um objeto durante suas edições, defina a igualdade com === e calcule o hash do seu ObjectIdentifier. O operador de identidade do Swift verifica se as referências apontam para a mesma instância. A Apple documenta ObjectIdentifier como um identificador hashable cujas comparações são válidas durante o tempo de vida da instância. Consulte Structures and Classes e ObjectIdentifier.

O código a seguir é um programa síncrono completo. Ele requer um Mac com as ferramentas de linha de comando do Swift disponíveis e usa apenas a biblioteca padrão do Swift, importada automaticamente. Salve-o como IdentityKeys.swift e execute swift IdentityKeys.swift no Terminal. Os exemplos foram analisados, mas não puderam ser executados no ambiente de pesquisa porque o Swift não estava disponível.

final class Rectangle: Hashable {
    var label: String
    var width: Int
    var height: Int

    init(label: String, width: Int, height: Int) {
        self.label = label
        self.width = width
        self.height = height
    }

    static func == (lhs: Rectangle, rhs: Rectangle) -> Bool {
        lhs === rhs
    }

    func hash(into hasher: inout Hasher) {
        hasher.combine(ObjectIdentifier(self))
    }
}

func demonstrateIdentityKeys() {
    let rectangle = Rectangle(label: "Tile A", width: 4, height: 3)
    let alias = rectangle
    let separate = Rectangle(label: "Tile A", width: 4, height: 3)

    var areas: [Rectangle: Int] = [
        rectangle: rectangle.width * rectangle.height
    ]

    rectangle.label = "Tile B"
    assert(areas[alias] == 12)
    assert(areas[separate] == nil)

    rectangle.width = 5
    assert(areas[rectangle] == 12) // Still the previous calculation.

    areas[rectangle] = rectangle.width * rectangle.height
    assert(areas[rectangle] == 15)
    assert(areas.count == 1)

    print("Renamed lookup: 12")
    print("Recalculated area: \(areas[rectangle]!)")
}

demonstrateIdentityKeys()

A saída esperada é Renamed lookup: 12, seguida de Recalculated area: 15. As asserções expressam o contrato útil: um alias encontra a entrada existente, um retângulo criado separadamente não a encontra e editar o objeto original não cria outra chave.

A alteração da largura deixa deliberadamente a área armazenada em 12 até que o programa atribua 15. A identidade permanece estável, mas o dicionário não acompanha dependências aritméticas nem recalcula valores. Decida se seu aplicativo atualiza o valor imediatamente após uma edição ou o remove até o próximo cálculo.

A igualdade por identidade é uma decisão que se aplica ao tipo inteiro. Toda comparação == nesta classe passa a significar “mesma instância”. Se o modelo já define a igualdade pelas dimensões ou por outro valor, use um projeto de chave separado, em vez de alterar esse significado só para um dicionário.

Você também pode usar ObjectIdentifier diretamente como chave de um dicionário, mas o identificador, sozinho, não fornece o retângulo necessário para operações posteriores. Mantenha o objeto relevante vivo enquanto seu identificador for usado para comparação e remova as entradas obsoletas. O limite de tempo de vida documentado torna este um mecanismo de identidade restrito ao escopo e à memória. Consulte ObjectIdentifier.

Use um instantâneo de valores quando a entrada representar um cálculo

Para resultados de área reutilizáveis, as próprias dimensões costumam ser a chave apropriada. Dois retângulos com medidas 4 por 3 podem compartilhar o mesmo resultado, mesmo que sejam objetos separados com rótulos diferentes. Alterar as dimensões deve selecionar outra entrada.

As estruturas do Swift têm semântica de valor, e o compilador pode sintetizar Hashable quando suas propriedades armazenadas são hashable. Um instantâneo com inteiros imutáveis, portanto, oferece uma chave direta para este cálculo. Consulte Structures and Classes e Hashable.

Execute esta alternativa completa de forma independente com swift SnapshotKeys.swift. As entradas sintéticas são inteiros positivos e pequenos, então o exemplo não precisa de uma interface genérica para entrada numérica.

final class EditableRectangle {
    var label: String
    var width: Int
    var height: Int

    init(label: String, width: Int, height: Int) {
        self.label = label
        self.width = width
        self.height = height
    }
}

struct AreaInputs: Hashable, Sendable {
    let width: Int
    let height: Int
}

func demonstrateSnapshotKeys() {
    let rectangle = EditableRectangle(
        label: "Tile A", width: 4, height: 3
    )
    var areas: [AreaInputs: Int] = [:]

    let original = AreaInputs(
        width: rectangle.width, height: rectangle.height
    )
    areas[original] = original.width * original.height

    rectangle.label = "Tile B"
    rectangle.width = 5

    let current = AreaInputs(
        width: rectangle.width, height: rectangle.height
    )
    assert(areas[original] == 12)
    assert(areas[current] == nil)

    areas[current] = current.width * current.height
    assert(areas[current] == 15)
    assert(areas.count == 2)

    print("Original area: \(areas[original]!)")
    print("Current area: \(areas[current]!)")
}

demonstrateSnapshotKeys()

A saída esperada é Original area: 12 e, em seguida, Current area: 15. Ambos os resultados continuam acessíveis porque o programa inseriu dois instantâneos distintos. O resultado antigo ainda descreve corretamente as dimensões antigas.

Inclua no instantâneo todas as entradas que afetam o cálculo. O rótulo não importa para a área de um retângulo, por isso foi excluído aqui. Se um cálculo posterior depender de outra configuração, adicione-a à chave. Caso contrário, cálculos diferentes poderão compartilhar uma entrada incorretamente. Esse é um requisito de projeto do cálculo, independente de a chave estar em conformidade com Hashable.

Uma estrutura que apenas encapsula uma referência a uma classe mutável não cria automaticamente um instantâneo congelado desse objeto. Copie os valores relevantes para o instantâneo, como neste exemplo. Decida também quando descartar instantâneos antigos, caso edições repetidas acumulem resultados desnecessários.

Quando você deve usar um ID imutável?

Use um ID quando várias instâncias de objeto devam acessar a mesma entrada lógica. Para esta ferramenta sintética, um ID inteiro imutável pode bastar: armazene os resultados em [Int: Int], use o let id: Int do retângulo para acessar os resultados e mantenha os rótulos e as dimensões editáveis fora da chave.

Nesse projeto, dois objetos com o ID 7 acessam intencionalmente a mesma entrada do dicionário. Suas regras de criação devem garantir que retângulos lógicos distintos não recebam o mesmo ID por engano. Atualizar uma chave existente substitui o valor associado, conforme documentado em Dictionary.

Uma tabela de áreas baseada em IDs ainda exige uma política de atualização quando as dimensões mudam. Se você quiser resultados para cada conjunto distinto de entradas, use instantâneos. Se quiser apenas o resultado mais recente para cada retângulo lógico, um ID com recálculo explícito é mais adequado.

Dicionários Foundation ou atores resolvem o problema das chaves mutáveis?

Foundation tem um contrato de inserção diferente. NSMutableDictionary.setObject(_:forKey:) exige uma chave NSCopying e a copia durante a inserção. Uma cópia da chave pode estabelecer um instantâneo quando sua implementação de cópia fornece um estado estável para a chave. As consultas posteriores devem corresponder a essa cópia armazenada. Consulte setObject(_:forKey:) da Apple.

Não conclua com base nesse comportamento que o dicionário nativo do Swift faz uma cópia independente de uma instância de classe usada como chave. A atribuição de classes no Swift tem semântica de referência. Para chaves personalizadas do Foundation, verifique em conjunto o comportamento de cópia, igualdade e hash; o requisito de hash do Foundation continua valendo.

Concorrência é uma questão separada. Atores coordenam o acesso ao seu estado mutável isolado, e Sendable descreve valores que podem atravessar domínios de concorrência segundo seus requisitos. Nenhum dos dois altera o significado da igualdade de uma chave nem corrige uma chave cujo estado definidor muda após a inserção. Consulte o capítulo sobre concorrência do Swift e a documentação de Sendable da Apple.

O exemplo do instantâneo declara explicitamente Sendable porque seus inteiros armazenados atendem aos requisitos do protocolo para tipos de valor. Se depois você mover os cálculos de área para um ator, envie um instantâneo completo e deixe que o ator mantenha o dicionário de resultados. O retângulo mutável pode permanecer dentro do domínio de isolamento atual. Estes exemplos síncronos não precisam de ator.

Verifique o contrato antes de confiar em uma consulta

Use uma sequência pequena e determinística para validar o projeto escolhido:

  1. Insira uma entrada e confirme a consulta inicial.
  2. Altere uma propriedade excluída da igualdade e do hash; confirme que a mesma entrada continua acessível.
  3. Compare um alias com um objeto ou instantâneo criado separadamente; confirme que os resultados correspondem ao significado pretendido para a igualdade.
  4. Altere uma entrada do cálculo; confirme que o resultado é atualizado, removido ou acessado por meio de um novo instantâneo.
  5. Confira a quantidade de entradas do dicionário para garantir que a substituição e a inserção funcionem como esperado.

Se uma implementação existente altera o estado que define a chave, corrija o modelo da chave antes de confiar em novas consultas. Para este pequeno cálculo local, reconstruir os resultados a partir de uma lista separada de entradas sintéticas é mais claro do que tratar operações posteriores no dicionário como um mecanismo de reparo. Preserve entradas originais suficientes para recalcular, em vez de descartar silenciosamente uma entrada que você não consegue encontrar.

Escolha identidade para uma associação com um objeto ativo, um ID imutável para um item lógico e um instantâneo de valores para um cálculo. Depois, defina explicitamente a política de atualização dos resultados. Juntas, essas decisões mantêm as consultas relevantes conforme o retângulo editável muda.

Fontes