¿Cómo anula correctamente isEqual:
en Objective-C? La "captura" parece ser que si dos objetos son iguales (según lo determinado por el isEqual:
método), deben tener el mismo valor hash.
La sección de Introspección de la Guía de Fundamentos de Cocoa tiene un ejemplo sobre cómo anular isEqual:
, copiado de la siguiente manera, para una clase llamada MyWidget
:
- (BOOL)isEqual:(id)other {
if (other == self)
return YES;
if (!other || ![other isKindOfClass:[self class]])
return NO;
return [self isEqualToWidget:other];
}
- (BOOL)isEqualToWidget:(MyWidget *)aWidget {
if (self == aWidget)
return YES;
if (![(id)[self name] isEqual:[aWidget name]])
return NO;
if (![[self data] isEqualToData:[aWidget data]])
return NO;
return YES;
}
Comprueba la igualdad del puntero, luego la igualdad de clase, y finalmente compara los objetos usando isEqualToWidget:
, que solo comprueba las propiedades name
y data
. Lo que el ejemplo no muestra es cómo anular hash
.
Supongamos que hay otras propiedades que no afectan la igualdad, por ejemplo age
. No debería el hash
método puede anular tal que sólo name
y data
afecta el hash? Y si es así, ¿cómo harías eso? Simplemente agregue los hashes de name
y data
? Por ejemplo:
- (NSUInteger)hash {
NSUInteger hash = 0;
hash += [[self name] hash];
hash += [[self data] hash];
return hash;
}
¿Es eso suficiente? ¿Hay una mejor técnica? ¿Qué pasa si tienes primitivos, como int
? Convertirlos NSNumber
para obtener su hash? O estructuras como NSRect
?
( Pedo cerebral : Originalmente escribí "bitwise O" junto con ellos |=
. Significaba agregar).
fuente
if (![other isKindOfClass:[self class]])
- Esto técnicamente significa que la igualdad no será conmutativa. Es decir, A = B no significa B = A (por ejemplo, si una es una subclase de la otra)Respuestas:
Empezar con
Entonces por cada primitivo que hagas
Para los objetos, usa 0 para nulo y, de lo contrario, su código hash.
Para booleanos usas dos valores diferentes
Explicación y Atribución
Este no es el trabajo de tcurdt, y los comentarios pedían más explicaciones, por lo que creo que una edición para la atribución es justa.
Este algoritmo se popularizó en el libro "Java efectivo", y el capítulo correspondiente se puede encontrar en línea aquí . Ese libro popularizó el algoritmo, que ahora es un valor predeterminado en una serie de aplicaciones Java (incluido Eclipse). Sin embargo, se derivó de una implementación aún más antigua que se atribuye de diversas maneras a Dan Bernstein o Chris Torek. Ese antiguo algoritmo originalmente flotaba en Usenet, y cierta atribución es difícil. Por ejemplo, hay algunos comentarios interesantes en este código de Apache (busque sus nombres) que haga referencia a la fuente original.
En pocas palabras, este es un algoritmo de hashing simple muy antiguo. No es el más eficiente, y ni siquiera se ha demostrado matemáticamente que sea un "buen" algoritmo. Pero es simple, y mucha gente lo ha usado durante mucho tiempo con buenos resultados, por lo que tiene mucho apoyo histórico.
fuente
Solo estoy recogiendo Objective-C, así que no puedo hablar específicamente para ese idioma, pero en los otros idiomas que uso si dos instancias son "Iguales", deben devolver el mismo hash; de lo contrario, tendrá todo problemas al intentar usarlos como claves en una tabla hash (o cualquier colección de tipo diccionario).
Por otro lado, si 2 instancias no son iguales, pueden o no tener el mismo hash; es mejor si no lo tienen. Esta es la diferencia entre una búsqueda O (1) en una tabla hash y una búsqueda O (N): si todos sus hash chocan, puede encontrar que buscar en su tabla no es mejor que buscar en una lista.
En términos de mejores prácticas, su hash debería devolver una distribución aleatoria de valores para su entrada. Esto significa que, por ejemplo, si tiene un doble, pero la mayoría de sus valores tienden a agruparse entre 0 y 100, debe asegurarse de que los hashes devueltos por esos valores se distribuyan uniformemente en todo el rango de posibles valores hash . Esto mejorará significativamente su rendimiento.
Existen varios algoritmos de hash, incluidos varios enumerados aquí. Intento evitar crear nuevos algoritmos hash, ya que puede tener grandes implicaciones de rendimiento, por lo que usar los métodos hash existentes y hacer una combinación de algún tipo de bit a bit como lo hace en su ejemplo es una buena manera de evitarlo.
fuente
Por ejemplo:
Solución encontrada en http://nshipster.com/equality/ por Mattt Thompson (¡que también mencionó esta pregunta en su publicación!)
fuente
Encontré este hilo extremadamente útil al proporcionar todo lo que necesitaba para implementar mis métodos
isEqual:
yhash
con un solo truco. Al probar las variables de instancia de objeto enisEqual:
el código de ejemplo, se usa:Esto falló repetidamente ( es decir , devolvió NO ) sin un error, cuando supe que los objetos eran idénticos en la prueba de mi unidad. La razón fue que una de las
NSString
variables de instancia era nula, por lo que la declaración anterior fue:y dado que nil responderá a cualquier método, esto es perfectamente legal pero
devuelve nil , que es NO , por lo tanto, cuando tanto el objeto como el objeto de prueba tenían un objeto nil , se considerarían no iguales ( es decir ,
isEqual:
devolverían NO ).Esta solución simple fue cambiar la instrucción if a:
De esta manera, si sus direcciones son las mismas, omite la llamada al método, sin importar si ambas son nulas o si ambas apuntan al mismo objeto, pero si no es nula o apuntan a diferentes objetos, se llama al comparador de manera apropiada.
Espero que esto le ahorre a alguien unos minutos de rascarse la cabeza.
fuente
La función hash debería crear un valor semi-único que no sea probable que choque o coincida con el valor hash de otro objeto.
Aquí está la función hash completa, que se puede adaptar a las variables de instancia de sus clases. Utiliza NSUInteger's en lugar de int para compatibilidad en aplicaciones de 64/32 bits.
Si el resultado se convierte en 0 para diferentes objetos, corre el riesgo de colisionar hashes. El colisionar hash puede provocar un comportamiento inesperado del programa cuando se trabaja con algunas de las clases de recopilación que dependen de la función hash. Asegúrese de probar su función hash antes de usarla.
fuente
result = prime * result + [self isSelected] ? yesPrime : noPrime;
. Luego descubrí que esto se estaba configurandoresult
en (por ejemplo)1231
, supongo que debido a que el?
operador tiene prioridad. He arreglado el problema agregando entre paréntesis:result = prime * result + ([self isSelected] ? yesPrime : noPrime);
La manera fácil pero ineficiente es devolver lo mismo
-hash
valor para cada instancia. De lo contrario, sí, debe implementar hash basado solo en objetos que afectan la igualdad. Esto es complicado si utiliza comparaciones laxas-isEqual:
(por ejemplo, comparaciones de cadenas que no distinguen entre mayúsculas y minúsculas). Para ints, generalmente puede usar el int en sí, a menos que se compare con NSNumbers.No utilice | =, sin embargo, se saturará. Use ^ = en su lugar.
Dato curioso al azar:
[[NSNumber numberWithInt:0] isEqual:[NSNumber numberWithBool:NO]]
pero[[NSNumber numberWithInt:0] hash] != [[NSNumber numberWithBool:NO] hash]
. (rdar: // 4538282, abierto desde el 05 de mayo de 2006)fuente
Recuerde que solo necesita proporcionar hash que sea igual cuando
isEqual
sea cierto. CuandoisEqual
es falso, el hash no tiene que ser desigual, aunque presumiblemente lo sea. Por lo tanto:Mantenga el hash simple. Elija una variable miembro (o pocos miembros) que sea la más distintiva.
Por ejemplo, para CLPlacemark, el nombre solo es suficiente. Sí, hay 2 o 3 distintivos CLPlacemark con exactamente el mismo nombre, pero son raros. Usa ese hash.
...
Tenga en cuenta que no me molesto en especificar la ciudad, el país, etc. El nombre es suficiente. Quizás el nombre y la ubicación CL.
El hash se debe distribuir uniformemente. Por lo tanto, puede combinar varias variables de miembros utilizando el símbolo de intercalación ^ (signo xor)
Entonces es algo como
De esa manera, el hash se distribuirá uniformemente.
Entonces, ¿qué hacer en la matriz?
De nuevo, simple. No tiene que hacer hash a todos los miembros de la matriz. Lo suficiente como para trocear el primer elemento, el último elemento, el recuento, tal vez algunos elementos intermedios, y eso es todo.
fuente
Espera, seguramente una forma mucho más fácil de hacer esto es anular primero
- (NSString )description
y proporcionar una representación de cadena del estado de su objeto (debe representar todo el estado de su objeto en esta cadena).Luego, solo proporcione la siguiente implementación de
hash
:Esto se basa en el principio de que "si dos objetos de cadena son iguales (según lo determinado por el método isEqualToString:), deben tener el mismo valor hash".
Fuente: Referencia de clase NSString
fuente
description
, no veo por qué esto es inferior a cualquiera de las soluciones más votadas. Puede que no sea la solución matemáticamente más elegante, pero debería ser suficiente. Como Brian B. afirma (la respuesta más votada en este momento): "Trato de evitar crear nuevos algoritmos hash" - ¡de acuerdo! - Yo solohash
elNSString
!description
incluye la dirección del puntero. Entonces, esto hace dos instancias diferentes de la misma clase que son iguales con diferentes hash, lo que viola la suposición básica de que dos objetos iguales tienen el mismo hash.Los contratos de igualdad y hash están bien especificados e investigados a fondo en el mundo de Java (consulte la respuesta de @ mipardi), pero todas las mismas consideraciones deberían aplicarse a Objective-C.
Eclipse hace un trabajo confiable al generar estos métodos en Java, así que aquí hay un ejemplo de Eclipse portado a mano a Objective-C:
Y para una subclase
YourWidget
que agrega una propiedadserialNo
:Esta implementación evita algunas dificultades de subclasificación en la muestra
isEqual:
de Apple:other isKindOfClass:[self class]
es asimétrica para dos subclases diferentes deMyWidget
. La igualdad debe ser simétrica: a = b si y solo si b = a. Esto podría solucionarse fácilmente cambiando la prueba aother isKindOfClass:[MyWidget class]
, entonces todas lasMyWidget
subclases serían mutuamente comparables.isKindOfClass:
prueba de subclase evita que las subclases se anulenisEqual:
con una prueba de igualdad refinada. Esto se debe a que la igualdad debe ser transitiva: si a = by a = c, entonces b = c. Si unaMyWidget
instancia se compara igual a dosYourWidget
instancias, entonces esasYourWidget
instancias deben comparar igual entre sí, incluso si suserialNo
difieren.El segundo problema se puede solucionar solo considerando que los objetos son iguales si pertenecen exactamente a la misma clase, de ahí que
[self class] != [object class]
prueba aquí. Para las clases de aplicación típicas , este parece ser el mejor enfoque.Sin embargo, ciertamente hay casos en los que la
isKindOfClass:
prueba es preferible. Esto es más típico de las clases de framework que de las clases de aplicación. Por ejemplo, cualquieraNSString
debe comparar igual a cualquier otroNSString
con la misma secuencia de caracteres subyacente, independientemente deNSString
/NSMutableString
distinción , y también independientemente de qué clases privadasNSString
están involucradas en el grupo de clases.En tales casos,
isEqual:
debe tener un comportamiento bien definido y bien documentado, y debe quedar claro que las subclases no pueden anular esto. En Java, la restricción de 'no anulación' se puede aplicar marcando los métodos igual y hashcode comofinal
, pero Objective-C no tiene equivalente.fuente
MyWidget
se entiende que no es un grupo de clases.Esto no responde directamente a su pregunta (en absoluto), pero he usado MurmurHash antes para generar hashes: murmurhash
Supongo que debería explicar por qué: murmurhash es muy rápido ...
fuente
He encontrado que esta página es una guía útil para anular los métodos de tipo igual y hash. Incluye un algoritmo decente para calcular códigos hash. La página está orientada a Java, pero es bastante fácil adaptarla a Objective-C / Cocoa.
fuente
Soy un novato del Objetivo C también, pero encontré un excelente artículo sobre identidad vs. igualdad en el Objetivo C aquí . Según mi lectura, parece que podría mantener la función hash predeterminada (que debería proporcionar una identidad única) e implementar el método isEqual para que compare los valores de los datos.
fuente
Equality vs Identity
de Karl Kraft es realmente bueno.isEqual:
, también debe anularhash
.Quinn se equivoca al decir que la referencia al soplo de soplo es inútil aquí. Quinn tiene razón en que quiere comprender la teoría detrás del hash. El murmullo destila gran parte de esa teoría en una implementación. Merece la pena explorar cómo aplicar esa implementación a esta aplicación en particular.
Algunos de los puntos clave aquí:
La función de ejemplo de tcurdt sugiere que '31' es un buen multiplicador porque es primo. Hay que demostrar que ser primo es una condición necesaria y suficiente. De hecho, 31 (y 7) probablemente no sean primos particularmente buenos porque 31 == -1% 32. Es probable que un multiplicador impar con aproximadamente la mitad de los bits establecidos y la mitad de los bits libres sea mejor. (La constante de multiplicación hash murmullo tiene esa propiedad).
Este tipo de función hash probablemente sería más fuerte si, después de multiplicar, el valor del resultado se ajustara mediante un desplazamiento y xor. La multiplicación tiende a producir los resultados de muchas interacciones de bits en el extremo superior del registro y resultados de baja interacción en el extremo inferior del registro. El desplazamiento y xor aumentan las interacciones en el extremo inferior del registro.
Establecer el resultado inicial en un valor donde aproximadamente la mitad de los bits son cero y aproximadamente la mitad de los bits son uno también tendería a ser útil.
Puede ser útil tener cuidado con el orden en que se combinan los elementos. Probablemente, primero se deben procesar booleanos y otros elementos donde los valores no están fuertemente distribuidos.
Puede ser útil agregar un par de etapas de codificación de bits adicionales al final del cálculo.
Si el hasm murmurio es realmente rápido para esta aplicación es una pregunta abierta. El soplo de murmullo premezcla los bits de cada palabra de entrada. Se pueden procesar varias palabras de entrada en paralelo, lo que ayuda a los cpus canalizados de múltiples problemas.
fuente
Combinando la respuesta de @ tcurdt con la respuesta de @ oscar-gomez para obtener nombres de propiedades , podemos crear una solución fácil de colocar tanto para isEqual como para hash:
Ahora, en su clase personalizada, puede implementar
isEqual:
yhash
:fuente
Tenga en cuenta que si está creando un objeto que puede mutar después de la creación, el valor hash no debe cambiar si el objeto se inserta en una colección. Hablando en términos prácticos, esto significa que el valor hash debe fijarse desde el punto de creación inicial del objeto. Consulte la documentación de Apple sobre el método -hash del protocolo NSObject para obtener más información:
Esto me parece una locura completa, ya que potencialmente hace que las búsquedas de hash sean mucho menos eficientes, pero supongo que es mejor errar por precaución y seguir lo que dice la documentación.
fuente
Lo siento si me arriesgo a hacer sonar un boffin completo aquí, pero ... ... nadie se molestó en mencionar que para seguir las 'mejores prácticas' definitivamente no deberías especificar un método igual que NO tomaría en cuenta todos los datos que posee tu objeto objetivo, por ejemplo los datos agregados a su objeto, en comparación con un asociado del mismo, deben tenerse en cuenta al implementar iguales. Si no desea tener en cuenta, diga 'edad' en una comparación, entonces debe escribir un comparador y usarlo para realizar sus comparaciones en lugar de isEqual :.
Si define un método isEqual: que realiza una comparación de igualdad arbitrariamente, corre el riesgo de que otro desarrollador, o incluso usted mismo, utilice este método una vez que haya olvidado el 'giro' en su interpretación igual.
Ergo, aunque esta es una gran pregunta sobre el hash, normalmente no es necesario redefinir el método de hash, probablemente deberías definir un comparador ad-hoc.
fuente