"Set" dovrebbe avere un metodo Get?


22

Diamo questa classe C # (sarebbe quasi la stessa in Java)

public class MyClass {
   public string A {get; set;}
   public string B {get; set;}

   public override bool Equals(object obj) {
        var item = obj as MyClass;

        if (item == null || this.A == null || item.A == null)
        {
            return false;
        }
        return this.A.equals(item.A);
   }

   public override int GetHashCode() {
        return A != null ? A.GetHashCode() : 0;
   }
}

Come puoi vedere, l'uguaglianza di due casi MyClassdipende Asolo da. Quindi ci possono essere due casi uguali, ma che contengono diverse informazioni nella loro Bproprietà.

In una libreria di raccolta standard di molte lingue (incluso C # e Java, ovviamente) c'è un Set( HashSetin C #), che è una raccolta, che può contenere al massimo un elemento da ogni set di istanze uguali.

Si possono aggiungere oggetti, rimuovere oggetti e verificare se il set contiene un oggetto. Ma perché è impossibile ottenere un determinato oggetto dal set?

HashSet<MyClass> mset = new HashSet<MyClass>();
mset.Add(new MyClass {A = "Hello", B = "Bye"});

//I can do this
if (mset.Contains(new MyClass {A = "Hello", B = "See you"})) {
    //something
}

//But I cannot do this, because Get does not exist!!!
MyClass item = mset.Get(new MyClass {A = "Hello", B = "See you"});
Console.WriteLine(item.B); //should print Bye

L'unico modo per recuperare il mio articolo è di scorrere su tutta la raccolta e verificare la parità di tutti gli elementi. Tuttavia, questo richiede O(n)tempo invece di O(1)!

Finora non ho trovato alcuna lingua che supporti ottenere da un set. Tutte le lingue "comuni" che conosco (Java, C #, Python, Scala, Haskell ...) sembrano progettate allo stesso modo: è possibile aggiungere elementi, ma non è possibile recuperarli. C'è qualche buona ragione per cui tutte queste lingue non supportano qualcosa di così facile e ovviamente utile? Non possono essere solo tutti sbagliati, giusto? Ci sono lingue che lo supportano? Forse ritirare un determinato oggetto da un set è sbagliato, ma perché?


Esistono alcune domande SO correlate:

/programming/7283338/getting-an-element-from-a-set

/programming/7760364/how-to-retrieve-actual-item-from-hashsett


12
C ++ std::setsupporta il recupero di oggetti, quindi non tutti i linguaggi "comuni" sono come li descrivi.
Ripristina Monica il

17
Se affermi (e codifichi) che "l'uguaglianza di due istanze di MyClass dipende solo da A", un'altra istanza che ha lo stesso valore A e B diversa è effettivamente "quella particolare istanza", poiché tu stesso hai definito che sono uguali e le differenze in B non contano; il contenitore è "autorizzato" a restituire l'altra istanza poiché è uguale.
Peteris,

7
Storia vera: in Java, molte Set<E>implementazioni sono solo Map<E,Boolean>all'interno.
corsiKa

10
parlando con la persona A : "Ciao, puoi portare la persona A proprio qui per favore"
Brad Thomas,

7
Questo rompe la riflessività ( a == bsempre vera) nel caso this.A == null. Il if (item == null || this.A == null || item.A == null)test è "esagerato" e controlla molto, probabilmente al fine di creare un codice artificialmente di "alta qualità". Vedo questo tipo di "controllo eccessivo" e di essere sempre eccessivamente corretto nella revisione del codice.
usr

Risposte:


66

Il problema qui non è che HashSetmanca un Getmetodo, è che il tuo codice non ha senso dal punto di vista del HashSettipo.

Quel Getmetodo è efficacemente "procurami questo valore, per favore", al quale la gente del framework .NET risponderebbe sensibilmente, "eh? Hai già quel valore <confused face />".

Se desideri archiviare articoli e recuperarli in base alla corrispondenza di un altro valore leggermente diverso, utilizza Dictionary<String, MyClass>come puoi quindi fare:

var mset = new Dictionary<String, MyClass>();
mset.Add("Hello", new MyClass {A = "Hello", B = "Bye"});

var item = mset["Hello"];
Console.WriteLine(item.B); // will print Bye

Le informazioni sull'uguaglianza trapelano dalla classe incapsulata. Se volessi cambiare il set di proprietà coinvolto Equals, dovrei cambiare il codice al di fuori MyClass...

Ebbene si, ma è perché MyClassfunziona con il principio del minimo stupore (POLA). Con quella funzionalità di uguaglianza incapsulata, è del tutto ragionevole supporre che il seguente codice sia valido:

HashSet<MyClass> mset = new HashSet<MyClass>();
mset.Add(new MyClass {A = "Hello", B = "Bye"});

if (mset.Contains(new MyClass {A = "Hello", B = "See you"})) 
{
    // this code is unreachable.
}

Per evitare ciò, MyClassdeve essere chiaramente documentato in merito alla sua strana forma di uguaglianza. Fatto ciò, non è più incapsulato e cambiare il modo in cui tale uguaglianza funziona spezzerebbe il principio aperto / chiuso. Ergo, non dovrebbe cambiare e quindi Dictionary<String, MyClass>è una buona soluzione per questo strano requisito.


2
@vojta, in tal caso, utilizzare Dictionary<MyClass, MyClass>come verrà quindi recuperare il valore in base a una chiave che utilizza MyClass.Equals.
David Arno,

8
Vorrei usare un Dictionary<MyClass, MyClass>fornito con un appropriato IEqualityComparer<MyClass>, ed estrarre la relazione di equivalenza da MyClassPerché MyClassdeve conoscere questa relazione sulle sue istanze?
Caleth,

16
@vojta e il commento lì: " meh. Sostituire l'implementazione di uguali in modo che gli oggetti non uguali siano" uguali "è il problema qui. Chiedere un metodo che dice" procurami l'oggetto identico a questo oggetto ", e poi aspettarsi che un oggetto non identico venga restituito sembra folle e facile da causare problemi di manutenzione "è perfetto. Questo è spesso il problema con SO: le risposte seriamente errate vengono annullate dalla gente che non ha riflettuto sulle implicazioni del loro desiderio di una soluzione rapida al loro codice non funzionante ...
David Arno,

6
@DavidArno: un po 'inevitabile, purché persistiamo nell'uso di linguaggi che distinguano tra uguaglianza e identità ;-) Se vuoi canonicalizzare oggetti uguali ma non identici, allora hai bisogno di un metodo che dice "non farmi lo stesso oggetto a questo oggetto ", ma" procurami l'oggetto canonico uguale a questo oggetto ". Chiunque pensi che HashSet.Get in queste lingue significhi necessariamente "procurami l'oggetto identico" è già gravemente in errore.
Steve Jessop,

4
Questa risposta ha molte affermazioni generali come ...reasonable to assume.... Tutto ciò potrebbe essere vero nel 99% dei casi, ma può essere utile la possibilità di recuperare un oggetto da un set. Il codice del mondo reale non può sempre aderire ai principi POLA ecc. Ad esempio, se si stanno deduplicando le stringhe senza distinzione tra maiuscole e minuscole, è possibile che si desideri ottenere l'elemento "master". Dictionary<string, string>è una soluzione alternativa, ma costa perf.
usr

24

Hai già l'elemento che è "nel" set - l'hai passato come chiave.

"Ma non è l'istanza che ho chiamato Aggiungi con" - Sì, ma hai affermato specificamente che erano uguali.

A Setè anche un caso speciale di un Map| Dictionary, con void come tipo di valore (anche i metodi inutili non sono definiti, ma non importa).

La struttura dei dati che stai cercando è un punto in Dictionary<X, MyClass>cui in Xqualche modo ottiene As dalle MyClass.

Il tipo di dizionario C # è carino in questo senso, in quanto consente di fornire un IEqualityComparer per le chiavi.

Per l'esempio fornito, avrei il seguente:

public class MyClass {
   public string A {get; set;}
   public string B {get; set;}
}

public class MyClassEquivalentAs : IEqualityComparer<MyClass>{
   public override bool Equals(MyClass left, MyClass right) {
        if (Object.ReferenceEquals(left, null) && Object.ReferenceEquals(right, null))
        {
            return true;
        }
        else if (Object.ReferenceEquals(left, null) || Object.ReferenceEquals(right, null))
        {
            return false;
        }
        return left.A == right.A;
   }

   public override int GetHashCode(MyClass obj) {
        return obj?.A != null ? obj.A.GetHashCode() : 0;
   }
}

Usato così:

var mset = new Dictionary<MyClass, MyClass>(new MyClassEquivalentAs());
var bye = new MyClass {A = "Hello", B = "Bye"};
var seeyou = new MyClass {A = "Hello", B = "See you"};
mset.Add(bye);

if (mset.Contains(seeyou)) {
    //something
}

MyClass item = mset[seeyou];
Console.WriteLine(item.B); // prints Bye

Esistono diverse situazioni in cui può essere vantaggioso per il codice che ha un oggetto corrispondente alla chiave, sostituirlo con un riferimento all'oggetto utilizzato come chiave. Ad esempio, se è noto che molte stringhe corrispondono a una stringa in una raccolta con hash, la sostituzione di riferimenti a tutte quelle stringhe con riferimenti a quella della raccolta potrebbe essere una vincita in termini di prestazioni.
supercat

@supercat oggi che si ottiene con a Dictionary<String, String>.
MikeFHay,

@MikeFHay: Sì, ma sembra un po 'inelegante dover memorizzare ogni riferimento di stringa due volte.
supercat

2
@supercat Se intendi una stringa identica , si tratta solo di interning della stringa. Usa le cose integrate. Se intendi un tipo di rappresentazione "canonica" (una che non può essere raggiunta usando tecniche semplici di modifica dei casi, ecc.), Sembra che tu abbia fondamentalmente bisogno di un indice (nel senso che i DB usano il termine). Non vedo un problema con l'archiviazione di ogni "forma non canonica" come chiave che si associa a una forma canonica. (Penso che questo si applichi altrettanto bene se la forma "canonica" non è una stringa.) Se questo non è ciò di cui stai parlando, allora mi hai completamente perso.
jpmc26,

1
Personalizzato Comparered Dictionary<MyClass, MyClass>è una soluzione pragmatica. In Java, lo stesso può essere ottenuto da TreeSeto TreeMappiù personalizzato Comparator.
Markus Kull,

19

Il tuo problema è che hai due concetti contraddittori di uguaglianza:

  • parità effettiva, in cui tutti i campi sono uguali
  • imposta l'uguaglianza di appartenenza, dove solo A è uguale

Se utilizzi la relazione di uguaglianza effettiva nel tuo set, il problema di recuperare un determinato elemento dal set non si pone - per verificare se un oggetto è nel set, hai già quell'oggetto. Pertanto, non è mai necessario recuperare un'istanza particolare da un set, supponendo che si stia utilizzando la relazione di uguaglianza corretta.

Potremmo anche sostenere che un insieme è un tipo di dati astratto che è definito esclusivamente dalla relazione S contains xo x is-element-of S("funzione caratteristica"). Se vuoi altre operazioni, non stai effettivamente cercando un set.

Ciò che accade abbastanza spesso - ma ciò che non è un insieme - è che raggruppiamo tutti gli oggetti in classi di equivalenza distinte . Gli oggetti in ciascuna di tali classi o sottogruppi sono solo equivalenti, non uguali. Possiamo rappresentare ogni classe di equivalenza attraverso qualsiasi membro di quel sottoinsieme e diventa quindi desiderabile recuperare quell'elemento rappresentativo. Questa sarebbe una mappatura dalla classe di equivalenza all'elemento rappresentativo.

In C #, un dizionario può usare una relazione di uguaglianza esplicita, credo. Altrimenti, tale relazione può essere implementata scrivendo una classe di wrapper rapido. pseudocodice:

// The type you actually want to store
class MyClass { ... }

// A equivalence class of MyClass objects,
// with regards to a particular equivalence relation.
// This relation is implemented in EquivalenceClass.Equals()
class EquivalenceClass {
  public MyClass instance { get; }
  public override bool Equals(object o) { ... } // compare instance.A
  public override int GetHashCode() { ... } // hash instance.A
  public static EquivalenceClass of(MyClass o) { return new EquivalenceClass { instance = o }; }
}

// The set-like object mapping equivalence classes
// to a particular representing element.
class EquivalenceHashSet {
  private Dictionary<EquivalenceClass, MyClass> dict = ...;
  public void Add(MyClass o) { dict.Add(EquivalenceClass.of(o), o)}
  public bool Contains(MyClass o) { return dict.Contains(EquivalenceClass.of(o)); }
  public MyClass Get(MyClass o) { return dict.Get(EquivalenceClass.of(o)); }
}

"recupera un'istanza particolare da un set" Penso che ciò significherebbe che cosa intendi più direttamente se hai cambiato "istanza" in "membro". Solo un piccolo suggerimento. =) +1
jpmc26

7

Ma perché è impossibile ottenere un determinato oggetto dal set?

Perché non è questo ciò che serve.

Vorrei riformulare l'esempio.

"Ho un HashSet in cui voglio archiviare gli oggetti MyClass e voglio essere in grado di ottenerli utilizzando la proprietà A che è uguale alla proprietà A dell'oggetto".

Se sostituisci "HashSet" con "Collezione", "oggetti" con "Valori" e "proprietà A" con "Chiave", la frase diventa:

"Ho una collezione in cui voglio archiviare i valori di MyClass e voglio essere in grado di ottenerli utilizzando la chiave che è uguale alla chiave dell'oggetto".

Quello che viene descritto è un dizionario. La vera domanda che viene posta è "Perché non posso trattare HashSet come un dizionario?"

La risposta è che non sono usati per la stessa cosa. Il motivo per usare un set è garantire l'unicità dei suoi singoli contenuti, altrimenti potresti semplicemente usare un Elenco o un array. Il comportamento descritto nella domanda è lo scopo di un dizionario. Tutti i progettisti linguistici non hanno fatto un casino. Non forniscono un metodo get perché se hai l'oggetto ed è nel set, sono equivalenti, il che significa che "otterresti" un oggetto equivalente. Sostenere che HashSet dovrebbe essere implementato in modo tale da poter "ottenere" oggetti non equivalenti che hai definito uguale è un non-inizio quando le lingue forniscono altre strutture di dati che ti consentono di farlo.

Una nota su OOP e commenti / risposte sull'uguaglianza. Va bene avere la chiave del mapping come proprietà / membro del valore memorizzato in un dizionario. Ad esempio: avere una guida come chiave e anche la proprietà utilizzata per il metodo equals è perfettamente ragionevole. Ciò che non è ragionevole è avere valori diversi per il resto delle proprietà. Trovo che se sto andando in quella direzione, probabilmente dovrò ripensare la mia struttura di classe.


6

Non appena esegui l'override è uguale a quello che preferisci, ignora l'hashcode. Non appena hai fatto ciò, la tua "istanza" non dovrebbe mai più cambiare lo stato interno.

Se non si sostituisce uguale a e hashcode l'identità dell'oggetto VM viene utilizzata per determinare l'uguaglianza. Se si inserisce questo oggetto in un set, è possibile ritrovarlo.

La modifica di un valore di un oggetto utilizzato per determinare l'uguaglianza porterà alla non tracciabilità di questo oggetto nelle strutture basate sull'hash.

Quindi un Setter su A è pericoloso.

Ora non hai B che non partecipa all'uguaglianza. Il problema qui non è semanticamente tecnicamente. Perché tecnicamente cambiare B è neutrale al fatto di uguaglianza. Semanticamente B deve essere qualcosa di simile a una bandiera "versione".

Il punto è:

Se si hanno due oggetti uguali a A ma non B, si presuppone che uno di questi oggetti sia più recente dell'altro. Se B non ha informazioni sulla versione, questo presupposto è nascosto nel tuo algoritmo QUANDO decidi di "sovrascrivere / aggiornare" questo oggetto in un set. Questa posizione del codice sorgente in cui ciò accade potrebbe non essere ovvia, quindi uno sviluppatore avrà difficoltà a identificare la relazione tra l'oggetto X e l'oggetto Y che differisce da X in B.

Se B ha informazioni sulla versione, si presuppone che in precedenza fosse derivato implicitamente solo dal codice. Ora puoi vedere che l'oggetto Y è una versione più recente di X.

Pensa a te stesso: la tua identità rimane per tutta la vita, forse alcune proprietà cambiano (ad es. Il colore dei tuoi capelli ;-)). Certo puoi presumere che se hai due foto, una con i capelli castani e una con i capelli grigi, potresti essere più giovane sulla foto con i capelli castani. Ma forse hai colorato i tuoi capelli? Il problema è: potresti sapere che hai colorato i capelli. Possono altri? Per mettere questo in un contesto valido devi introdurre l'età della proprietà (versione). Allora sei semanticamente esplicito e non religioso.

Per evitare l'operazione nascosta "sostituzione vecchio con nuovo oggetto" un Set non dovrebbe avere un metodo get. Se vuoi un comportamento come questo, devi renderlo esplicito rimuovendo il vecchio oggetto e aggiungendo il nuovo oggetto.

A proposito: cosa dovrebbe significare se passi un oggetto uguale all'oggetto che vuoi ottenere? Questo non ha senso. Mantieni pulita la tua semantica e non farlo, anche se tecnicamente nessuno ti ostacolerà.


7
"Non appena esegui l'override equivale a sostituire l'hashcode. Non appena lo hai fatto, la tua" istanza "non dovrebbe mai più cambiare lo stato interno." Questa affermazione vale +100, proprio lì.
David Arno,

+1 per indicare i pericoli dell'uguaglianza e dell'hashcode in base allo stato mutabile
Hulk,

3

In particolare in Java, HashSetinizialmente è stato implementato utilizzando HashMapcomunque, e semplicemente ignorando il valore. Quindi il progetto iniziale non prevedeva alcun vantaggio nel fornire un metodo get a HashSet. Se si desidera archiviare e recuperare un valore canonico tra vari oggetti uguali, utilizzare semplicemente un valore HashMapte stesso.

Non mi sono tenuto aggiornato con tali dettagli di implementazione, quindi non posso dire se questo ragionamento si applica ancora integralmente in Java, figuriamoci in C # ecc. Ma anche se HashSetvenissi reimplementato per usare meno memoria di HashMap, in ogni caso sarebbe un cambiamento sostanziale per aggiungere un nuovo metodo Setall'interfaccia. Quindi è un bel dolore per un guadagno che non tutti considerano degno di avere.


Bene, in Java potrebbe essere possibile fornire defaultun'implementazione per farlo in modo non-break. Semplicemente non sembra un cambiamento terribilmente utile.
Hulk,

@Hulk: potrei sbagliarmi, ma penso che qualsiasi implementazione predefinita sarebbe terribilmente inefficiente, dal momento che, come dice l'interrogante, "L'unico modo per recuperare il mio articolo è iterare su tutta la collezione e verificare la parità di tutti gli elementi". Quindi un buon punto, puoi farlo in un modo retrocompatibile, ma aggiungendo un gotcha che la funzione get risultante garantisce solo di essere eseguita in O(n)confronti anche se la funzione hash sta dando una buona distribuzione. Quindi le implementazioni Setche sovrascrivono l'implementazione predefinita nell'interfaccia, incluso HashSet, potrebbero fornire una migliore garanzia.
Steve Jessop,

D'accordo - Non penso che sarebbe una buona idea. Ci sarebbero comunque delle precedenti per questo tipo di comportamento - List.get (int index) o - per scegliere un'implementazione predefinita aggiunta di recente a List.sort . L'interfaccia fornisce garanzie di massima complessità, ma alcune implementazioni potrebbero fare molto meglio di altre.
Hulk,

2

Esiste una lingua principale il cui set ha la proprietà desiderata.

In C ++, std::setè un set ordinato. Ha un .findmetodo che cerca l'elemento in base all'operatore di ordinamento <o alla bool(T,T)funzione binaria fornita. È possibile utilizzare find per implementare l'operazione get desiderata.

In effetti, se il bool(T,T) funzione fornita ha un flag specifico su di essa ( is_transparent), è possibile passare oggetti di un tipo diverso per il quale la funzione presenta sovraccarichi. Ciò significa che non è necessario applicare il secondo campo "fittizio" per i dati, ma solo assicurarsi che l'operazione di ordinamento che si utilizza possa ordinare tra la ricerca e i tipi contenuti nel set.

Ciò consente un efficiente:

std::set< std::string, my_string_compare > strings;
strings.find( 7 );

dove my_string_comparecapisce come ordinare numeri interi e stringhe senza prima convertire l'intero in una stringa (a un costo potenziale).

Per unordered_set(il set di hash di C ++), non esiste un flag trasparente equivalente (ancora). Devi passare da a Ta anunordered_set<T>.find metodo a. Potrebbe essere aggiunto, ma gli hash richiedono ==e un hash, a differenza dei set ordinati che richiedono solo un ordine.

Lo schema generale è che il contenitore eseguirà la ricerca, quindi fornirà un "iteratore" a quell'elemento all'interno del contenitore. A quel punto puoi ottenere l'elemento all'interno dell'insieme, o eliminarlo, ecc.

In breve, non tutti i contenitori standard delle lingue hanno i difetti che descrivi. I contenitori basati su iteratore della libreria standard C ++ non lo sono, e almeno alcuni dei contenitori esistevano prima di qualsiasi altra lingua che hai descritto, e la possibilità di ottenere un risultato ancora più efficiente di come descrivi è stata aggiunta. Non c'è niente di sbagliato nel tuo design o nel volere quell'operazione; i progettisti dei set che state usando semplicemente non hanno fornito quell'interfaccia.

Contenitori standard C ++ progettati per avvolgere in modo pulito le operazioni di basso livello dell'equivalente codice C arrotolato a mano, progettato per abbinarsi a come si potrebbe scrivere in modo efficiente nell'assemblaggio. I suoi iteratori sono un'astrazione di puntatori in stile C. Le lingue che menzioni si sono tutte allontanate dai puntatori come concetto, quindi non hanno utilizzato l'astrazione iteratore.

È possibile che il fatto che C ++ non abbia questo difetto sia un incidente di progettazione. Il percorso incentrato sull'iteratore significa che per interagire con un elemento in un contenitore associativo si ottiene prima un iteratore per l'elemento, quindi si utilizza quell'iteratore per parlare della voce nel contenitore.

Il costo è che ci sono regole di invalidazione dell'iterazione che devi tenere traccia e alcune operazioni richiedono 2 passaggi anziché uno (che rende più rumoroso il codice client). Il vantaggio è che la solida astrazione consente un uso più avanzato rispetto a quelli che i progettisti dell'API avevano in mente in origine.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.