Subclase de NSObject en Swift: hash vs hashValue, isEqual vs ==

Al crear subclases de NSObject en Swift, ¿debe anular hash o implementar Hashable? Además, ¿debería anular isEqual: o implementar ==?

NSObject ya se ajusta al protocolo Hashable :

 extension NSObject : Equatable, Hashable { /// The hash value. /// /// **Axiom:** `x == y` implies `x.hashValue == y.hashValue` /// /// - Note: the hash value is not guaranteed to be stable across /// different invocations of the same program. Do not persist the /// hash value across program runs. public var hashValue: Int { get } } public func ==(lhs: NSObject, rhs: NSObject) -> Bool 

No pude encontrar una referencia oficial, pero parece que hashValue llama al método hash de NSObjectProtocol , y == llama al método isEqual: (del mismo protocolo). ¡Mira la actualización al final de la respuesta!

Para NSObject subclases de NSObject , la forma correcta parece ser anular hash e isEqual: y aquí hay un experimento que demuestra que:

1. hashValue y ==

 class ClassA : NSObject { let value : Int init(value : Int) { self.value = value super.init() } override var hashValue : Int { return value } } func ==(lhs: ClassA, rhs: ClassA) -> Bool { return lhs.value == rhs.value } 

Ahora crea dos instancias diferentes de la clase que se consideran “iguales” y colócalas en un conjunto:

 let a1 = ClassA(value: 13) let a2 = ClassA(value: 13) let nsSetA = NSSet(objects: a1, a2) let swSetA = Set([a1, a2]) print(nsSetA.count) // 2 print(swSetA.count) // 2 

Como puede ver, tanto NSSet como Set tratan los objetos como diferentes. Este no es el resultado deseado. Las matrices también tienen resultados inesperados:

 let nsArrayA = NSArray(object: a1) let swArrayA = [a1] print(nsArrayA.indexOfObject(a2)) // 9223372036854775807 == NSNotFound print(swArrayA.indexOf(a2)) // nil 

Establecer puntos de interrupción o agregar salida de depuración revela que el operador anulado == nunca se llama. No sé si esto es un error o un comportamiento intencionado.

2. isEqual: hash e isEqual:

 class ClassB : NSObject { let value : Int init(value : Int) { self.value = value super.init() } override var hash : Int { return value } override func isEqual(object: AnyObject?) -> Bool { if let other = object as? ClassB { return self.value == other.value } else { return false } } } 

Para Swift 3, la definición de isEqual: cambió a

 override func isEqual(_ object: Any?) -> Bool { ... } 

Ahora todos los resultados son los esperados:

 let b1 = ClassB(value: 13) let b2 = ClassB(value: 13) let nsSetB = NSSet(objects: b1, b2) let swSetB = Set([b1, b2]) print(swSetB.count) // 1 print(nsSetB.count) // 1 let nsArrayB = NSArray(object: b1) let swArrayB = [b1] print(nsArrayB.indexOfObject(b2)) // 0 print(swArrayB.indexOf(b2)) // Optional(0) 

Actualización: el comportamiento ahora está documentado en Interacción con las API de Objective-C en la referencia “Uso de Swift con Cocoa y Objective-C”:

La clase NSObject solo realiza una comparación de identidad, por lo que debe implementar su propio método isEqual: en las clases que se derivan de la clase NSObject.

Como parte de la implementación de la igualdad para su clase, asegúrese de implementar la propiedad hash de acuerdo con las reglas en Comparación de objetos.

Implementa Hashable , que también requiere que implementes el operador == para tu tipo. Estos se utilizan para muchas cosas útiles en la biblioteca estándar Swift como la función indexOf que solo funciona en colecciones de un tipo que implementa Equatable o el tipo Set que solo funciona con tipos que implementan Hashable .