Mantén estables las propiedades usadas para la igualdad y el hash mientras un objeto sea una clave de diccionario. Para un cálculo local en Swift en tu Mac, elige la identidad del objeto, un ID inmutable o una instantánea de valores según el significado que quieras darle a la búsqueda.

Este tutorial usa rectángulos sintéticos y pequeños cálculos de área que se mantienen íntegramente en memoria. El objetivo práctico es permitir que cambien la etiqueta o las dimensiones de un rectángulo sin perder la entrada de diccionario asociada y sin confundir una búsqueda exitosa con un cálculo actualizado.

¿Por qué cambiar un objeto puede interrumpir una búsqueda en un diccionario?

El Dictionary de Swift es una tabla hash cuyas claves cumplen el protocolo Hashable. El subíndice basado en una clave devuelve un opcional porque la clave solicitada podría no existir. El hash ayuda a localizar una entrada; la igualdad determina qué claves se consideran equivalentes. Consulta la documentación de Dictionary de Apple.

El contrato de Hashable relaciona esas operaciones: las instancias iguales deben proporcionar los mismos componentes esenciales a Hasher, en el mismo orden. Al implementar una clave personalizada, usa las propiedades que definen la igualdad en hash(into:). Apple describe este requisito en Hashable.

Una clave de tipo referencia añade otra consideración. Asignar una instancia de una clase pasa una referencia al objeto existente, en lugar de crear un objeto independiente. Declarar la referencia con let no congela sus propiedades mutables. Estas son reglas del lenguaje documentadas en Estructuras y clases.

Supongamos que una clase de rectángulo compara y calcula el hash de su etiqueta editable. Insertas el objeto cuando su etiqueta es \"Tile A\" y luego la cambias a \"Tile B\". La referencia almacenada ahora expone un estado distinto en las propiedades que definen la clave. La ubicación original en el diccionario y la búsqueda posterior ya no concuerdan de forma estable.

La regla de diseño se deriva de los contratos de las tablas hash y de igualdad: mantén estable el estado que define la igualdad de una clave mientras esta pertenezca al diccionario. Apple establece explícitamente el requisito correspondiente para los objetos de Foundation: el hash de un objeto no debe cambiar mientras pertenezca a una colección que lo use para determinar la ubicación. Consulta NSObjectProtocol.hash.

No bases una prueba en la suposición de que toda mutación no válida devuelve inmediatamente nil. Una vez que se incumple el contrato de la clave, que una búsqueda parezca tener éxito no demuestra que el diseño funcione. En su lugar, prueba las reglas de igualdad previstas y el comportamiento estable.

¿Qué tipo de clave se ajusta a la tarea?

Decide qué representa una entrada de diccionario antes de implementar la conformidad. Para una herramienta que calcula el área de rectángulos, estas tres preguntas conducen a diseños distintos:

¿Qué debería representar una entrada? Clave adecuada ¿Qué ocurre cuando cambia el rectángulo?
Este objeto de rectángulo concreto Clave de clase basada en la identidad La entrada sigue asociada a esa instancia
Un rectángulo lógico, identificado independientemente de su objeto actual Valor de ID inmutable La entrada sigue asociada al ID
Un cálculo para determinadas dimensiones Instantánea inmutable de dimensiones Si cambian las dimensiones, se obtiene una clave de búsqueda distinta

Esta tabla ayuda a tomar decisiones de diseño a partir de las reglas de identidad, valores y hashing de Swift. También separa dos obligaciones: mantener accesible una asociación y mantener actualizado un resultado calculado. Una clave de identidad estable resuelve la primera; volver a calcular o usar una instantánea completa de los datos de entrada resuelve la segunda.

Usa la identidad del objeto cuando la entrada corresponda a una instancia

Para una asociación en memoria que deba seguir a un objeto mientras se modifica, define la igualdad con === y calcula el hash de su ObjectIdentifier. El operador de identidad de Swift comprueba si las referencias apuntan a la misma instancia. Apple documenta ObjectIdentifier como un identificador hashable cuyas comparaciones son válidas durante la vida de la instancia. Consulta Estructuras y clases y ObjectIdentifier.

El siguiente programa es completo y síncrono. Necesitas un Mac con las herramientas de línea de comandos de Swift disponibles; solo usa la biblioteca estándar de Swift, que se importa automáticamente. Guárdalo como IdentityKeys.swift y ejecuta swift IdentityKeys.swift en Terminal. Se revisaron los ejemplos, pero no se pudieron ejecutar en el entorno de investigación porque Swift no estaba disponible.

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()

La salida esperada es Renamed lookup: 12, seguida de Recalculated area: 15. Las aserciones expresan el contrato útil: un alias encuentra la entrada existente, un rectángulo creado por separado no la encuentra y editar el objeto original no crea otra clave.

El cambio de ancho deja deliberadamente el área almacenada en 12 hasta que el programa asigna 15. La identidad sigue siendo estable, pero el diccionario no observa las dependencias aritméticas ni vuelve a calcular los valores. Decide si tu aplicación actualiza el valor justo después de una edición o lo elimina hasta el siguiente cálculo.

La igualdad por identidad es una decisión que afecta a todo el tipo. Todas las comparaciones con == en esta clase ahora significan «la misma instancia». Si el modelo ya define la igualdad según las dimensiones u otro valor, utiliza un diseño de clave independiente en lugar de cambiar ese significado solo para un diccionario.

También puedes usar ObjectIdentifier directamente como clave de un diccionario, pero el identificador por sí solo no proporciona el rectángulo que necesitas para operaciones posteriores. Mantén vivo el objeto pertinente mientras uses su identificador para hacer comparaciones y elimina las entradas obsoletas. El límite de vida documentado hace de este un mecanismo de identidad en memoria cuyo alcance es limitado. Consulta ObjectIdentifier.

Usa una instantánea de valores cuando la entrada represente un cálculo

Para reutilizar resultados de áreas, a menudo conviene usar las dimensiones como clave. Dos rectángulos de 4 por 3 pueden compartir el mismo resultado, aunque sean objetos distintos con etiquetas diferentes. Si cambian las dimensiones, debe seleccionarse otra entrada.

Las estructuras de Swift tienen semántica de valor y el compilador puede sintetizar Hashable cuando sus propiedades almacenadas son hashables. Por tanto, una instantánea que contenga enteros inmutables ofrece una clave sencilla para este cálculo. Consulta Estructuras y clases y Hashable.

Ejecuta esta alternativa completa por separado con swift SnapshotKeys.swift. Sus datos sintéticos de entrada son enteros positivos pequeños, así que el ejemplo no necesita una interfaz de entrada numérica de propósito general.

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()

La salida esperada es Original area: 12 y luego Current area: 15. Ambos resultados siguen estando disponibles porque el programa insertó dos instantáneas distintas. El resultado anterior sigue describiendo correctamente las dimensiones anteriores.

Incluye en la instantánea todos los datos de entrada que afecten al cálculo. La etiqueta no influye en el área del rectángulo, así que aquí se excluye. Si un cálculo posterior depende de otra configuración, añádela a la clave. De lo contrario, cálculos distintos podrían compartir incorrectamente una entrada. Este requisito corresponde al diseño del cálculo, independientemente de que la clave cumpla Hashable.

Una estructura que solo envuelve una referencia a una clase mutable no crea automáticamente una instantánea congelada de ese objeto. Copia los valores pertinentes en la instantánea, como hace este ejemplo. Decide también cuándo descartar las instantáneas antiguas si las ediciones repetidas acumulan resultados que ya no necesitas.

¿Cuándo conviene usar un ID inmutable?

Usa un ID cuando varias instancias de objetos deban acceder a la misma entrada lógica. Para esta herramienta sintética, puede bastar un ID entero inmutable: almacena los resultados en [Int: Int], usa let id: Int del rectángulo para acceder a ellos y mantén las etiquetas y dimensiones editables fuera de la clave.

Con este diseño, dos objetos con el ID 7 acceden intencionalmente a la misma entrada del diccionario. Las reglas de creación deben garantizar que rectángulos lógicos distintos no reciban accidentalmente el mismo ID. Al actualizar una clave existente, se reemplaza su valor asociado, tal como se documenta en Dictionary.

Una tabla de áreas basada en ID también requiere una política de actualización cuando cambian las dimensiones. Si quieres conservar resultados para cada conjunto distinto de datos de entrada, usa instantáneas. Si solo quieres el resultado más reciente de cada rectángulo lógico, un ID más un recálculo explícito es una mejor opción.

¿Los diccionarios de Foundation o los actores resuelven el problema de las claves mutables?

Foundation tiene un contrato de inserción diferente. NSMutableDictionary.setObject(_:forKey:) requiere una clave NSCopying y la copia durante la inserción. Una copia de la clave puede crear una instantánea si su implementación de copia proporciona un estado estable. Las búsquedas posteriores deben coincidir con esa copia almacenada. Consulta el método setObject(_:forKey:) de Apple.

No deduzcas de este comportamiento que el diccionario nativo de Swift crea una copia independiente de una instancia de clase usada como clave. La asignación de clases en Swift tiene semántica de referencia. Para claves personalizadas de Foundation, comprueba conjuntamente el comportamiento de copia, igualdad y hash; sigue aplicándose el requisito de hash de Foundation.

La concurrencia es una cuestión aparte. Los actores coordinan el acceso a su estado mutable aislado, y Sendable describe valores que pueden cruzar dominios de concurrencia si cumplen sus requisitos. Ninguno de los dos cambia el significado de la igualdad de una clave ni repara una clave cuyo estado definitorio cambia tras la inserción. Consulta el capítulo sobre concurrencia de Swift y la documentación de Sendable de Apple.

El ejemplo de instantáneas declara Sendable explícitamente porque sus enteros almacenados cumplen los requisitos del protocolo para tipos de valor. Si más adelante trasladas los cálculos de área a un actor, envía una instantánea completa y deja que el actor sea dueño de su diccionario de resultados. El rectángulo mutable puede permanecer en su dominio de aislamiento actual. Estos ejemplos síncronos no necesitan un actor.

Comprueba el contrato antes de confiar en una búsqueda

Usa una secuencia pequeña y determinista para validar el diseño elegido:

  1. Inserta una entrada y confirma que la búsqueda inicial funciona.
  2. Cambia una propiedad excluida de la igualdad y el hash; confirma que la misma entrada sigue estando disponible.
  3. Compara un alias con un objeto o una instantánea creados por separado; confirma que los resultados coinciden con el significado de igualdad previsto.
  4. Cambia un dato de entrada del cálculo; confirma que el resultado se actualiza, se elimina o se obtiene mediante una nueva instantánea.
  5. Comprueba el recuento del diccionario para verificar que los reemplazos e inserciones se comportan como esperas.

Si una implementación existente modifica el estado que define la clave, corrige el modelo de claves antes de confiar en otras búsquedas. Para este pequeño cálculo local, es más claro reconstruir los resultados a partir de una lista independiente de datos sintéticos de entrada que tratar las operaciones posteriores del diccionario como un mecanismo de reparación. Conserva suficientes datos originales para volver a calcular, en lugar de descartar silenciosamente una entrada que no puedes encontrar.

Elige la identidad para una asociación con un objeto activo, un ID inmutable para un elemento lógico y una instantánea de valores para un cálculo. Después, define explícitamente la política de actualización de resultados. Con esas decisiones, las búsquedas seguirán teniendo sentido aunque cambie el rectángulo editable.

Fuentes