Anulando GetHashCode en una estructura mutable - ¿Qué NO hacer?

7

Estoy usando XNA Framework para hacer un proyecto de aprendizaje. Tiene una estructura que expone un valor de X e Y; con el propósito de optimización, rompe las reglas para el diseño correcto de la estructura, ya que es una estructura mutable .

Como Marc Gravell , John Skeet , y Eric Lippert señalan en sus publicaciones respectivas sobre GetHashCode() (que reemplaza el Punto), esto es algo bastante malo, ya que si los valores de un objeto cambian mientras está contenido en un hashmap (es decir, consultas LINQ), se puede "perder".

Sin embargo, estoy haciendo mi propia estructura Point3D , siguiendo el diseño de Point como guía. Por lo tanto, también es una estructura mutable que reemplaza a GetHashCode() . La única diferencia es que Mine expone e int para los valores de X, Y y Z, pero es fundamentalmente el mismo. Las firmas están abajo:

public struct Point3D : IEquatable<Point3D>
{
    public int X;
    public int Y;
    public int Z;

    public static bool operator !=(Point3D a, Point3D b) { }
    public static bool operator ==(Point3D a, Point3D b) { }

    public Point3D Zero { get; }

    public override int GetHashCode() { }
    public override bool Equals(object obj) { }
    public bool Equals(Point3D other) { }
    public override string ToString() { }
}

He intentado romper mi estructura en la forma en que lo describen, es decir, almacenándola en un List<Point3D> , así como cambiando el valor a través de un método que usa ref , pero no encontré el comportamiento del que advierten. (¿Tal vez un puntero podría permitirme romperlo?).

¿Estoy siendo demasiado cauteloso en mi enfoque, o debo estar bien para usarlo como está?

    
pregunta Kyle Baran 13.05.2014 - 05:06

2 respuestas

11

Vamos a descomponerlo.

  

¿Una estructura mutable con campos públicos es una buena idea?

No. Ya sabes que no lo es, pero de todos modos estás eligiendo jugar con fuego mientras caminas sobre hielo delgado. No aconsejaría utilizar Point como modelo. Los tipos de valor deben ser lógicamente valores, y los valores no cambian, cambian variables .

Dicho esto, puede haber buenas razones de rendimiento para hacer lo que estás haciendo; Solo lo haría si tuviera pruebas empíricas claras de que no había otra forma de cumplir mis objetivos de rendimiento.

  

Si pongo una estructura mutable en un diccionario y luego la muto, ¿se puede "perder" la estructura en el código hash?

La pregunta no se puede responder porque supone una falsedad. Usted no puede poner una estructura mutable en un diccionario y luego mutarla. Preguntando "¿qué pasa cuando hago algo imposible?" no permite respuestas sobre las cuales puede tomar decisiones de ingeniería.

  

¿Qué sucede cuando intento mutar una estructura mutable que he puesto en un diccionario?

Los tipos de valor se copian por valor; es por eso que se llaman "tipos de valor". Cuando recuperas el valor del diccionario, haces una copia. Si luego lo mutas, mutas la copia. Si intentas mutarlo directamente cambiando un campo, el compilador te dirá que estás mutando una copia y que la mutación se perderá.

  

Entonces, ¿cuál es el peligro real de poner una estructura mutable en un diccionario?

El peligro de poner un tipo de valor mutable en un diccionario es que las mutaciones se pierden, no que el objeto se pierda en el diccionario. Es decir, cuando dices:

struct Counter 
{ 
  public int x; 
  public void Increment() { x = x + 1; } 
  ...
}

...

var c = new Counter();
var d = new Dictionary<string, Counter>();
d["hello"] = c;
c.Increment();
Console.WriteLine(c.x);
Console.WriteLine(d["hello"].x);
d["hello"].Increment();
Console.WriteLine(c.x);
Console.WriteLine(d["hello"].x);

luego, el incremento a c se conserva pero no se copia al diccionario, y el incremento de la copia del diccionario se pierde porque está hecho a una copia, no al diccionario.

Esto es muy confuso para las personas, por eso debes evitarlo.

    
respondido por el Eric Lippert 03.06.2014 - 22:31
5

No veo ningún problema en particular al escribir una estructura como esta, una vez que entiendas completamente las concesiones. El problema es (presumiblemente) que desea poder almacenarlo en un Dict (o una colección similar) y necesita un HashCode.

Una cosa que no mencionaste es: ¿tus puntos tienen identidad? Es decir, si tiene dos puntos con el mismo valor, ¿son el mismo punto? [Como dos enteros, ambos con el valor 7 son el mismo entero. Solo hay un entero con el valor 7.]

La mutabilidad implica identidad. Si un Point3D tiene identidad, entonces su HashCode se deriva de su identidad. Podría usar un puntero this o algo así como un GUID.

Si el Point3D no tiene identidad, entonces su HashCode se deriva mejor de su valor. Si cambia el valor (por ejemplo, reutilizando los valores de un grupo), mejor asegúrese de que no esté en una colección similar a Dict en ese momento o se perderá.

Obviamente, si no usas colecciones basadas en hash, entonces el punto es discutible.

Si lo haces, considera este código.

public struct ValuePoint {
  public int x;
  public int y;
  public override int GetHashCode() { return (x * 1000 + y) % 97; }
  public ValuePoint(int _x, int _y) { x = _x; y = _y; }
}
public struct IdentityPoint {
  static int nextkey = 0;
  int key;
  public int x;
  public int y;
  public override int GetHashCode() { return key; }
  public IdentityPoint(int _x, int _y) { key = ++nextkey; x = _x; y = _y; }
}
class Program {
  Dictionary<ValuePoint, int> dictv = new Dictionary<ValuePoint,int>();
  Dictionary<IdentityPoint, int> dicti = new Dictionary<IdentityPoint,int>();
  HashSet<ValuePoint> setv = new HashSet<ValuePoint>();
  HashSet<IdentityPoint> seti = new HashSet<IdentityPoint>();

  void exec() {
    ValuePoint vp = new ValuePoint(3, 4);
    dictv[vp] = 777;
    vp.x++;
    dictv[vp] = 888;
    IdentityPoint ip = new IdentityPoint(3, 4);
    dicti[ip] = 777;
    ip.x++;
    dicti[ip] = 888;
  }

ValuePoint usa su valor como su clave hash. Si cambia el valor ( x++ ), el valor original se 'pierde' y el valor asignado '888' agrega una nueva clave en lugar de actualizar la existente.

IdentityPoint usa una clave única. Si cambia el valor ( x++ ), la entrada original aún se encuentra y se sobrescribe con el valor asignado '888'.

Este es un ejemplo artificial, pero ilustra el punto sobre el valor frente a la identidad. [El código es malo pero lo suficientemente bueno para el propósito.]

@EricLippert es un escritor muy respetado a quien recuerdo desde los primeros días de COM y el diseño de .NET y su consejo es sensato. Creo que él pierde el punto al usar un diccionario en el que la clave es una cadena. El problema de GetHashCode () solo surge cuando el objeto mutable en sí es la clave.

    
respondido por el david.pfx 13.05.2014 - 07:13

Lea otras preguntas en las etiquetas