Saltar al contenido

Swift, ¿cómo implementar el protocolo Hashable basado en la referencia de objetos?

Solución:

Si está trabajando con clases y no con estructuras, puede usar el ObjectIdentifier estructura Tenga en cuenta que también debe definir == para su clase con el fin de ajustarse a Equatable (Hashable lo requiere). Se vería así:

class MyClass: Hashable { }

func ==(lhs: MyClass, rhs: MyClass) -> Bool {
    return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}

class MyClass: Hashable {
    var hashValue: Int {
        return ObjectIdentifier(self).hashValue
    }
}

En Swift, el tipo debe ajustarse a Hashable y Equatable para que se utilice en una estructura de datos como un Dictionary o un Set. Sin embargo, puede agregar “conformidad automática” utilizando el “identificador de objeto” del objeto. En el siguiente código, implementé una clase reutilizable para hacer esto automáticamente.

Tenga en cuenta que Swift 4.2 cambió la forma en que Hashable está implementado, por lo que ya no anula hashValue. En cambio, anula hash(into:).

open class HashableClass {
    public init() {}
}

// MARK: - <Hashable>

extension HashableClass: Hashable {

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

    // `hashValue` is deprecated starting Swift 4.2, but if you use 
    // earlier versions, then just override `hashValue`.
    //
    // public var hashValue: Int {
    //    return ObjectIdentifier(self).hashValue
    // }
}

// MARK: - <Equatable>

extension HashableClass: Equatable {

    public static func ==(lhs: HashableClass, rhs: HashableClass) -> Bool {
        return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
    }
}

Para usar, solo tome su clase y subclase HashableClass, ¡entonces todo debería funcionar!

class MyClass: HashableClass {

}

Como se muestra en la respuesta aceptada aquí, varias implementaciones usarán extensiones en sus tipos de clase para implementar Hashable. El problema con ese enfoque es tienes que repetir eso para cada tipo que definas.

Aquí hay un ejemplo de ese enfoque …

extension SomeClass : Hashable {

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

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

extension SomeOtherClass : Hashable {

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

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

extension YetAnotherClass : Hashable {

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

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

¡Eso es mucho código duplicado!

Lo que propongo es revertir las cosas. En lugar de ampliar los tipos de clases individuales, amplía la Hashable protocolo en sí, luego restringirlo a AnyClass. Esto se aplica automáticamente a todos clases que simplemente especifican la conformidad con el protocolo, no se necesita una implementación específica de la clase.

Así es como se ve este enfoque …

extension Hashable where Self: AnyObject {

    func hash(into hasher: inout Hasher) {
        hasher.combine(ObjectIdentifier(self))
    }
}
    
extension Equatable where Self: AnyObject {

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

Con los dos anteriores en su lugar, ahora podemos hacer esto …

extension SomeClass : Hashable {}
extension SomeOtherClass : Hashable {}
extension YetAnotherClass : Hashable {}

Sin código repetido. Solo una conformidad con un protocolo.

Por supuesto Hashable también te da Equatable implícitamente, por lo que todos estos ahora también funcionan …

let a = SomeClass()
let b = a
let msg = (a == b)
    ? "They match! :)"
    : "They don't match. :(" 
print(msg)

// Prints They match! :)

Nota: esto no interferirá con las clases que implementan Hashable directamente, ya que una definición específica de clase es más explícita, por lo que tiene prioridad y estos pueden coexistir pacíficamente.

Dando un paso más y hablando solo de Equatable, si quieres implícitamente (es decir, no se necesita una conformidad manual con un protocolo) hacer que todos los tipos de objetos se implementen Equatable usando la identidad para probar la igualdad, algo que personalmente me pregunto por qué no hace esto de forma predeterminada (y que aún puede anular por tipo si es necesario), puede usar genéricos con un operador de igualdad definido globalmente, estableciendo la restricción para AnyObject

Aquí está el código …

func == <T:AnyObject>(lhs: T, rhs: T) -> Bool {
    return lhs === rhs
}

func != <T:AnyObject>(lhs: T, rhs: T) -> Bool {
    return !(lhs == rhs)
}

Nota: Para completar, también debe proporcionar explícitamente la != operador como diferente al definir el = operador en una extensión, el compilador no lo sintetizará por usted.

Con lo anterior en su lugar, ahora puede hacer esto …

class Foo {} // Note no protocols or anything else specified. Equality 'just works'

let a = Foo()
let b = a
var msg = (a == b)
    ? "They match! :)"
    : "They don't match. :(" 
print(msg)

// Prints They match! :)

let c = Foo()
var msg = (a == c)
    ? "They don't match! :)"
    : "They match. :(" 
print(msg)

// Prints They don't match! :)

Como se mencionó anteriormente, también puede usar versiones de igualdad específicas de tipo. Esto se debe a que tienen prioridad sobre los AnyObject versión ya que son más específicas y, por lo tanto, pueden coexistir pacíficamente con la igualdad de referencia predeterminada proporcionada anteriormente.

Aquí hay un ejemplo que asume que lo anterior está en su lugar, pero aún define una versión explícita de igualdad para Laa, basado únicamente en id.

class Laa {
    init(_ id:String){
        self.id = id
    }
    let id:String
}

// Override implicit object equality and base it on ID instead of reference
extension Laa : Equatable {

    static func == (lhs:Laa, rhs:Laa) -> Bool {
        return lhs.id == rhs.id
    }
}

Precaución: Si el objeto en el que está anulando la igualdad también implementa Hashable, debe asegurarse de que los valores hash correspondientes también sean iguales como por definición, los objetos que son iguales deben producir el mismo valor hash.

Dicho esto, si tienes el Hashable extensión limitada a AnyObject de la parte superior de esta publicación y simplemente etiquete su clase con Hashable obtendrá la implementación predeterminada que se basa en la identidad del objeto, por lo que no coincidirá con diferentes instancias de clase que comparten la misma ID (y, por lo tanto, se consideran iguales), por lo que debe asegurarse explícitamente de implementar la función hash también. El compilador no captará esto por ti.

Nuevamente, solo tiene que hacer esto si está anulando la igualdad y tu clase implementa Hashable. Si es así, aquí se explica cómo hacerlo …

Implementar hash en Hee para seguir la relación de igualdad / hash:

extension Hee : Hashable {

    func hash(into hasher: inout Hasher) {
        hasher.combine(id) // Easiest to simply use ID here since that's what Equatable above is based on
    }
}

Y finalmente, así es como se usa …

let hee1 = Hee("A")
let hee2 = Hee("A")
let msg2 = (hee1 == hee2)
    ? "They match! :)"
    : "They don't match. :("
print(msg2)

// Prints 'They match! :)'

let set = Set<Hee>()
set.append(hee1)
set.append(hee2)
print("Set Count: (set.count)")

// Prints 'Set Count: 1'
¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *