2011-02-01 16 views
11

Cuando se agrega un objeto a la clase .NET System.Collections.Generic.Dictionary, el código hash de la clave se almacena internamente y se usa para comparaciones posteriores. Cuando el código hash cambia después de su inserción inicial en el diccionario, a menudo se vuelve "inaccesible" y puede sorprender a sus usuarios cuando una comprobación de existencia, incluso utilizando la misma referencia, devuelve falso (código de muestra a continuación).¿Se ha roto el diccionario o debería GetHashCode() solo base en miembros inmutables?

La documentación GetHashCode dice:

El método GetHashCode para un objeto siempre debe devolver el mismo código hash, siempre y cuando no hay ninguna modificación en el estado del objeto que determina el valor de retorno de del objeto es igual método.

Así, de acuerdo con los documentos GetHashCode, el código hash puede cambiar cada vez que se cambia el estado equality -determinar, sin embargo, la aplicación Dictionary no apoyar esto.

¿La implementación actual del diccionario .NET está rota porque ignora incorrectamente los permisos de hashcode? ¿Debería GetHashCode() basarse solamente en miembros inmutables? O, ¿hay algo más para romper una posible falsa dicotomía?

class Hashable 
{ 
    public int PK { get; set; } 

    public override int GetHashCode() 
    { 
     if (PK != 0) return PK.GetHashCode(); 
     return base.GetHashCode(); 
    } 

    public override bool Equals(object obj) 
    { 
     return Equals(obj as Hashable); 
    } 

    public virtual bool Equals(Hashable other) 
    { 
     if (other == null) return false; 
     else if (ReferenceEquals(this, other)) return true; 
     else if (PK != 0 && other.PK != 0) return Equals(PK, other.PK); 
     return false; 
    } 

    public override string ToString() 
    { 
     return string.Format("Hashable {0}", PK); 
    } 
} 

class Test 
{ 
    static void Main(string[] args) 
    { 
     var dict = new Dictionary<Hashable, bool>(); 
     var h = new Hashable(); 
     dict.Add(h, true); 

     h.PK = 42; 
     if (!dict.ContainsKey(h)) // returns false, despite same reference 
      dict.Add(h, false); 
    } 
} 
+1

Si un diccionario admite llaves mutantes, tendría que engancharse en las teclas y de alguna manera ser notificado cada vez que cambie GetHashCode(). La implicación de hacer eso suena enorme, probablemente simplemente imposible. – Anonym

+0

Siempre asumí que el diccionario se basaba en algo así como un identificador o puntero de objeto subyacente (que aún no estoy seguro de que tenga .NET), pero tiene razón: HashCode es una opción de implementación perfecta para un diccionario. –

+0

De hecho, este diseño es * permitir * que use tipos más complejos como claves; solo necesitas diseñarlos adecuadamente. (Si usa un puntero de objeto, por ejemplo, podría terminar con duplicados en el diccionario debido a tener dos instancias de un tipo complejo como claves, aunque haya diseñado correctamente ese tipo complejo para representar, por ejemplo, una clave compuesta). –

Respuesta

26

No, simplemente no debe mutar una tecla (de una manera material) después de insertarla en un diccionario. Esto es por diseño, y la forma en que cada tabla hash que he usado funciona. Los documentos incluso especifican esto:

Siempre que un objeto se use como clave en el Dictionary<TKey, TValue>, no debe cambiar de ninguna manera que afecte su valor hash. Cada clave en un Dictionary<TKey, TValue> debe ser única según el comparador de igualdad del diccionario. Una clave no puede ser nula, pero puede ser un valor, si el tipo de valor TValue es un tipo de referencia.

Así que sólo va a sorprender a los usuarios que no lea la documentación :)

+1

+1, ¡maravilloso! .. – Aliostad

+0

Ok, merecía esa bofetada en la cara ... y leeré * todos * los documentos la próxima vez :). –

+0

Como un efecto secundario tonto, puedes mutar el objeto todo lo que quieras, siempre y cuando el código hash no se modifique (por ejemplo, el código hash depende solo de la clave de una entidad, no de sus valores) – SWeko

8

Para añadir a la respuesta de Jon, me acaba de dar énfasis a una cierta parte de los documentos que se citan:

método

el GetHashCode para un objeto debe volver consistentemente el mismo código hash siempre y cuando no hay modificación del estado del objeto que determina el valor de retorno de iguales del objeto método.

Ahora, allí ha infringido las reglas. Cambiaste PK, lo que no significa afectar el resultado de Equals (porque tiene que ReferenceEquals cheque de allí), pero el resultado de su GetHashCodehace cambio. Entonces esa es la respuesta simple.

Tomando el enfoque más conceptual, creo que se puede ver en ello como esto: si usted tiene anulados la Equals y GetHashCode comportamiento para su tipo, la propiedad luego de haber tomado el concepto de lo que significa para una instancia de este tipo es igual a otra.Y, de hecho, lo ha definido de forma que un objeto Hashable se puede cambiar en algo completamente diferente; es decir, algo que ya no se puede usar de la misma manera que antes (porque su código hash ha cambiado).

Considerado desde este punto de vista, después de hacer dict.Add(h, true), y luego cambiar h.PK, el diccionario no se contienen el objeto referenciado por h más. Contiene algo else (que en realidad no existe en ninguna parte). Es como que el tipo que has definido es una serpiente que se ha despojado de su piel.

+0

Creo que simplemente complicas demasiado las cosas y la gente apenas entiende lo que escribiste aquí. Jon Skeet lo dijo todo. –

+0

@Al: Jaja ... lo siento? Pensé que estaba diciendo algo significativo; si sientes que esta respuesta es absurda o demasiado confusa, eres libre de rechazarla; No lo tomaré personalmente. Aunque * apreciaría * una explicación de cuál es la parte realmente problemática, si es posible (es decir, qué dije específicamente que lo incitó a escribir ese comentario). Tal vez solo me dije mal y podría ser más claro. –

+0

Me gusta su párrafo que comienza con "Tomar el enfoque más conceptual". Realmente no me gusta la parte citada de la especificación, ya que no veo ninguna base legítima para una clase con estado mutable visible para anular 'Object.Equals'. El concepto de equivalencia está bien definido para objetos de todos los tipos (clase y estructura); los objetos deben anular 'Object.Equals' para probar la equivalencia (aunque algunos tipos (por ej.' Decimal') anulan 'Object.Equals' para significar algo más), y' Object.GetHashCode() 'debe ser compatible con' Object.Equals '. – supercat

Cuestiones relacionadas