Override di GetHashCode in una struttura mutabile - Cosa NON fare?

7

Sto usando XNA Framework per realizzare un progetto di apprendimento. Ha una struttura Punto che espone un valore X e Y; ai fini dell'ottimizzazione, infrange le regole per il corretto design delle strutture, poiché è una struttura mutevole .

Come Marc Gravell , John Skeet e Eric Lippert indica nei rispettivi post circa GetHashCode() (che Point sovrascrive), questa è una cosa piuttosto negativa, poiché se i valori di un oggetto cambiano mentre è contenuto in una hashmap (ad esempio, query LINQ), può diventare "perso".

Tuttavia, sto creando la mia struttura Point3D , seguendo il disegno di Point come linea guida. Quindi, anche questa è una struttura mutevole che sovrascrive GetHashCode() . L'unica differenza è che la mia espone e int per i valori X, Y e Z, ma è fondamentalmente la stessa. Le firme sono di seguito:

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() { }
}

Ho provato a rompere la mia struttura nel modo in cui descrivono, ovvero memorizzandola in List<Point3D> , così come cambiando il valore tramite un metodo usando ref , ma non ho riscontrato il loro comportamento che mettono in guardia (forse un puntatore potrebbe permettermi di romperlo?).

Sono troppo prudente nel mio approccio, o dovrei essere d'accordo a usarlo così com'è?

    
posta Kyle Baran 13.05.2014 - 05:06
fonte

2 risposte

12

Scopriamolo.

Is a mutable struct with public fields a good idea?

No. Sai già che non lo è, ma hai scelto di giocare con il fuoco mentre camminavo su ghiaccio sottile comunque. Vi sconsigliamo di usare Point come modello. I tipi di valore dovrebbero essere logicamente valori, e i valori non cambiano, le variabili cambiano.

Detto questo, ci possono essere buoni motivi di rendimento per fare quello che stai facendo; Lo farei solo se avessi una chiara evidenza empirica che non c'era altro modo per raggiungere i miei obiettivi di rendimento.

If I put a mutable struct into a dictionary and then mutate it, can the struct be "lost" in the hash code?

La domanda non risponde perché presume una menzogna. non puoi mettere una struttura mutabile in un dizionario e poi mutarlo. Chiedere "cosa succede quando faccio qualcosa di impossibile?" non offre risposte su cui puoi prendere decisioni ingegneristiche.

What happens when I try to mutate a mutable struct that I've put into a dictionary?

I tipi di valore sono copiati per valore; è per questo che vengono chiamati "tipi di valore". Quando recuperi il valore dal dizionario, ne fai una copia. Se lo si modifica, si modifica la copia. Se provi a mutarlo direttamente cambiando un campo, il compilatore ti dirà che stai mormorando una copia e che la mutazione andrà persa.

So what's the real danger of putting a mutable struct into a dictionary?

Il rischio di mettere un tipo di valore mutabile in un dizionario è che le mutazioni vengono perse, non che l'oggetto si perda nel dizionario. Cioè, quando dici:

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);

quindi l'incremento in c viene conservato ma non copiato nel dizionario e l'incremento della copia del dizionario viene perso perché è fatto su una copia, non sul dizionario.

Questo è molto che confonde le persone, motivo per cui dovresti evitarlo.

    
risposta data 03.06.2014 - 22:31
fonte
5

Non vedo alcun problema particolare nello scrivere una struttura come questa, una volta compreso appieno i compromessi. Il problema è (presumibilmente) che vuoi essere in grado di memorizzarlo in un Dict (o raccolta simile) e hai bisogno di un HashCode.

Una cosa che non hai menzionato è: i tuoi punti hanno identità? Cioè, se hai due punti con lo stesso valore, sono lo stesso punto? [Come due interi entrambi con il valore 7 sono lo stesso numero intero. Esiste solo un numero intero con valore 7.]

La mutabilità implica identità. Se Point3D ha un'identità, il suo HashCode deriva dalla sua identità. Potresti usare un puntatore this o qualcosa di simile a un GUID.

Se Point3D non ha identità, il suo HashCode è meglio derivato dal suo valore. Se modifichi il valore (ad esempio riutilizzando i valori da un pool), assicurati che non si trovi in una raccolta di tipo Dict al momento o che vada perso.

Ovviamente, se non si utilizzano le raccolte basate su hash, il punto è moot.

Se lo fai, considera questo codice.

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 il suo valore come hashkey. Se modifichi il valore ( x++ ), il valore originale viene "perso" e il valore assegnato "888" aggiunge una nuova chiave anziché aggiornare quella esistente.

IdentityPoint utilizza una chiave univoca. Se modifichi il valore ( x++ ), la voce originale viene ancora trovata e sovrascritta con il valore assegnato '888'.

Questo è un esempio forzato, ma illustra il punto sul valore rispetto all'identità. [Il codice è cattivo ma abbastanza buono per lo scopo.]

@EricLippert è uno scrittore molto apprezzato che ricordo fin dai primi giorni della COM e del design di .NET e il suo consiglio è valido. Penso che manchi il punto usando un dizionario in cui la chiave è una stringa. Il problema di GetHashCode () si verifica solo quando l'oggetto mutevole è la chiave.

    
risposta data 13.05.2014 - 07:13
fonte

Leggi altre domande sui tag