Haz que la igualdad y el hash personalizados de Swift usen una única definición de valor inmutable. Este recorrido crea una clave de mosaico local cuyo nombre no distingue entre mayúsculas y minúsculas, mientras que sus dimensiones se mantienen exactas; después comprueba la igualdad, la pertenencia a conjuntos y la búsqueda en diccionarios.
Define exactamente cuándo dos mosaicos son iguales
Supón que estás creando un pequeño ejercicio de gráficos para Mac. Necesitas agrupar descripciones sintéticas de mosaicos por nombre, ancho y alto. La tarea consiste en implementar esa agrupación de forma coherente: Orbit y ORBIT con las mismas dimensiones deben producir claves iguales; cambiar cualquiera de las dimensiones debe producir una clave distinta.
Escribe el contrato antes de implementar cualquiera de los protocolos. En este ejemplo, dos claves son iguales cuando coinciden sus nombres plegados sin distinguir mayúsculas y minúsculas, sus anchos y sus altos. Las dimensiones son unidades de dibujo expresadas como enteros positivos. Los espacios iniciales y finales siguen siendo significativos. Esta es una regla ilustrativa de la aplicación, no un requisito impuesto por Swift.
| Entradas sintéticas | Relación esperada | Motivo |
|---|---|---|
Orbit, 40 × 20 y ORBIT, 40 × 20 |
Iguales | Se ignoran las mayúsculas y minúsculas |
Orbit, 40 × 20 y orbit, 41 × 20 |
Distintas | El ancho interviene en la igualdad |
Orbit, 40 × 20 y orbit, 40 × 21 |
Distintas | El alto interviene en la igualdad |
Orbit, 40 × 20 y orbit , 40 × 20 |
Distintas | Se conserva el espacio final |
Usa una clave específica cuando una regla de agrupación concreta difiera del valor completo de un mosaico mostrado. Conserva la grafía original para mostrarla en el modelo de presentación que lo rodea. Así evitas declarar intercambiables dos descripciones completas de mosaicos mientras sus nombres visibles difieren. La documentación de Equatable de Swift describe la igualdad en términos de sustituibilidad y exige reflexividad, simetría y transitividad. Equatable
Usa los mismos componentes para la igualdad y el hash
El protocolo Hashable de Swift hereda de Equatable. Su requisito esencial es que las instancias iguales proporcionen los mismos valores al hasher en el mismo orden. Los valores Hashable pueden servir como elementos de conjuntos y claves de diccionarios. Hashable
Un error frecuente es comparar nombres normalizados en ==, pero calcular el hash a partir de la grafía original. Así, los nombres tienen significados distintos en los dos métodos. Otro error es omitir el ancho en la igualdad e incluirlo en el hash. Ambos diseños permiten que valores declarados iguales proporcionen entradas de hash diferentes. Apple indica explícitamente que las implementaciones personalizadas deben usar las mismas propiedades significativas en ambos métodos. Adopting Common Protocols
En este ejemplo, incluye los componentes de igualdad en una estructura anidada privada. Deja que Swift sintetice la igualdad y el hash de esa estructura, y luego delega en ella ambas operaciones externas. Así, la lista de componentes queda declarada en un solo lugar y puedes inspeccionarla. Swift admite la conformidad sintetizada para estructuras cuyas propiedades almacenadas conforman los protocolos requeridos. Protocols — Adopting a Protocol Using a Synthesized Implementation
Si tu tipo ya almacena exactamente sus componentes de igualdad, normalmente basta con la conformidad sintetizada a Hashable. Los métodos externos explícitos de abajo muestran cómo mantener alineada una implementación personalizada existente sin conservar dos listas de campos independientes.
Ejecuta el ejemplo local completo en tu Mac
Necesitas tener instalado un conjunto de herramientas de Swift con Foundation. En Terminal, compruébalo con swift --version, tal como recomienda la documentación de inicio de Swift. Si Swift no está disponible, sigue la guía oficial de instalación para macOS antes de continuar. Getting Started, Install Swift — macOS
El siguiente bloque es un único comando de shell que contiene el programa Swift. Pega el bloque completo en Terminal. Proporciona el programa mediante la entrada estándar; el ejemplo solo realiza cálculos en memoria e imprime resultados. Se revisó el código Swift, pero no se pudo ejecutar en el entorno de investigación porque Swift no estaba disponible.
swift - <<'SWIFT'
import Foundation
struct TileKey: Hashable, Sendable {
private struct Components: Hashable, Sendable {
let foldedName: String
let width: Int
let height: Int
}
private let components: Components
init(name: String, width: Int, height: Int) {
precondition(width > 0 && height > 0,
"Tile dimensions must be positive")
let foldedName = name.folding(
options: [.caseInsensitive],
locale: Locale(identifier: "en_US_POSIX")
)
components = Components(
foldedName: foldedName,
width: width,
height: height
)
}
static func == (lhs: TileKey, rhs: TileKey) -> Bool {
lhs.components == rhs.components
}
func hash(into hasher: inout Hasher) {
hasher.combine(components)
}
}
let a = TileKey(name: "Orbit", width: 40, height: 20)
let b = TileKey(name: "ORBIT", width: 40, height: 20)
let c = TileKey(name: "orbit", width: 40, height: 20)
let wider = TileKey(name: "orbit", width: 41, height: 20)
let taller = TileKey(name: "orbit", width: 40, height: 21)
let spaced = TileKey(name: "orbit ", width: 40, height: 20)
let samples = [a, b, c, wider, taller, spaced]
precondition(a == b && b == c && a == c)
precondition(a != wider && a != taller && a != spaced)
// Check the equality laws over this finite sample.
for first in samples {
precondition(first == first, "Reflexivity failed")
for second in samples {
precondition((first == second) == (second == first),
"Symmetry failed")
if first == second {
precondition(first.hashValue == second.hashValue,
"Equal values have different hashes")
}
for third in samples where first == second && second == third {
precondition(first == third, "Transitivity failed")
}
}
}
let uniqueKeys = Set(samples)
precondition(uniqueKeys.count == 4)
precondition(uniqueKeys.contains(b))
let patterns = [a: "striped"]
precondition(patterns[b] == "striped")
print("Unique keys:", uniqueKeys.count)
print("Equivalent key found:", uniqueKeys.contains(b))
print("Pattern:", patterns[b] ?? "missing")
print("Sample contract checks passed")
SWIFT
Salida esperada:
Unique keys: 4
Equivalent key found: true
Pattern: striped
Sample contract checks passed
Las seis entradas forman cuatro grupos: las tres variantes de mayúsculas y minúsculas comparten una clave, mientras que la variante con el ancho cambiado, la variante con el alto cambiado y la que tiene un espacio final forman cada una otro grupo. La comprobación del diccionario pregunta si se puede recuperar un valor asociado a a usando la clave igual b. Estas comprobaciones ponen a prueba el comportamiento deseado de las colecciones sin depender de ningún valor numérico de hash concreto.
El inicializador se detiene deliberadamente si las dimensiones no son positivas. Esto es apropiado para este ejercicio sintético fijo, donde las dimensiones no válidas indican un error de programación. Si una interfaz de escritorio permite introducir dimensiones incompletas, valida esos datos antes de construir la clave o cambia el inicializador para que devuelva un opcional o lance un error. No sustituyas una dimensión en silencio, porque eso cambia el contrato de igualdad.
Explicita la regla de normalización de texto
El paso de normalización llama a folding(options:locale:) de Foundation a través de la API de cadenas de Swift. La API devuelve una cadena con las opciones de plegado especificadas aplicadas. Aquí solo se solicita .caseInsensitive. StringProtocol.folding(options:locale:)
Foundation documenta el plegado de mayúsculas y minúsculas, de ancho y de diacríticos como opciones distintas. También explica que la configuración regional puede cambiar los resultados del plegado y que el texto plegado puede no ser adecuado para mostrar. Por eso, el ejemplo proporciona una configuración regional fija y almacena el resultado plegado solo dentro de la clave. Los nombres sintéticos son deliberadamente sencillos. Esta configuración regional define el comportamiento de este ejercicio; no es una recomendación universal para todas las interfaces de nombres. NSString.folding(options:locale:)
La decisión de diseño útil consiste en normalizar una sola vez al construir la clave. Después, la igualdad y el hash usan el resultado almacenado, así que ninguna de las dos operaciones elige una configuración regional o transformación por su cuenta. Si más adelante la aplicación necesita otra política de comparación, crea claves con esa nueva política y vuelve a construir la colección en memoria afectada.
Antes de ampliar la normalización, añade ejemplos que distingan los grupos deseados. ¿Deben importar los espacios? ¿Deben seguir siendo distintas las letras acentuadas? ¿Los anchos de caracteres visualmente diferentes deben contar como un mismo nombre? Responde esas preguntas como reglas del producto y luego implementa una transformación que usen ambas operaciones.
Diagnostica una discrepancia sin confiar en los valores de hash
Sigue estos pasos al adaptar este diseño a un tipo existente:
- Escribe un par que deba compararse como igual y un par por cada campo que deba distinguir los valores.
- Rastrea los valores que realmente compara
==, incluidas la normalización y las propiedades derivadas. - Rastrea los valores que se proporcionan a
hasher.combine, incluido su orden. - Traslada la definición compartida a un único valor de componente inmutable o haz que ambas implementaciones usen explícitamente los mismos componentes.
- Comprueba las leyes de igualdad, los hashes de valores iguales, la pertenencia a conjuntos y la búsqueda en diccionarios con los mismos datos de prueba.
No implementes la igualdad comparando hashValue. Los valores iguales deben tener hashes iguales, pero que los hashes coincidan no demuestra que los valores sean iguales. Los componentes completos de igualdad siguen siendo la base de ==. Del mismo modo, una prueba que exija que cada par desigual tenga hashes diferentes exige más de lo que garantiza el contrato del hash. Hashable
Compara los hashes solo dentro de la ejecución actual al comprobar este contrato. Hasher de Swift usa una semilla aleatoria y su algoritmo puede cambiar entre versiones de la biblioteca estándar. Por eso, un entero impreso en la ejecución de ayer no sirve como resultado esperado para la ejecución de hoy. Hasher
Las comprobaciones de una muestra finita son útiles como pruebas de regresión, pero no demuestran el comportamiento para todas las cadenas y dimensiones posibles. También importa el argumento estructural: ambos métodos externos delegan en el mismo valor inmutable Components, cuyos campos almacenados expresan por completo el contrato elegido.
Mantén separados los requisitos de concurrencia y de igualdad
El ejemplo declara ambas estructuras como Sendable porque sus valores almacenados son cadenas, enteros y otra estructura transferible. Swift documenta que las estructuras transferibles deben contener propiedades almacenadas transferibles. La transferibilidad concierne al traslado de valores entre dominios de concurrencia; no define qué valores son iguales. Sendable, The Swift Programming Language — Concurrency
Este cálculo local no necesita tareas ni actores. Si más adelante pasa a formar parte de código de escritorio concurrente, mantén la igualdad y el hash como operaciones síncronas sobre los componentes almacenados de la clave. Considera cualquier colección mutable compartida como un asunto de diseño de concurrencia aparte. La guía del lenguaje describe los actores y el aislamiento como mecanismos para coordinar el acceso al estado mutable. The Swift Programming Language — Concurrency
También hay una cuestión de interoperabilidad con Foundation al adaptar este patrón: una subclase de NSObject usa el comportamiento heredado de igualdad y hash. La forma admitida por Apple para personalizarlo consiste en sobrescribir juntos isEqual(_:) y hash. La estructura Swift simple de este ejemplo usa == y hash(into:). Adopting Common Protocols
Para una clave de mosaico local, el resultado fácil de mantener consiste en una regla de comparación escrita, una única representación inmutable de esa regla y comprobaciones que ejerciten búsquedas reales en colecciones. Cuando cambie la definición, actualiza a la vez la representación de los componentes y sus ejemplos para que la igualdad y el hash sigan describiendo el mismo valor.
Fuentes
- Hashable — Apple
- Equatable — Apple
- Hasher — Apple
- Adopting Common Protocols — Apple
- folding(options:locale:) — StringProtocol — Apple
- folding(options:locale:) — NSString — Apple
- Sendable — Apple
- Protocols — The Swift Programming Language, Swift.org
- Concurrency — The Swift Programming Language, Swift.org
- Getting Started — Swift.org
- Install Swift — macOS — Swift.org