"Set" dovrebbe avere un metodo Get?

22

Abbiamo 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 istanze di MyClass dipende solo da A . Quindi ci possono essere due istanze che sono uguali, ma che contengono informazioni diverse nella loro proprietà B .

In una raccolta di raccolte standard di molte lingue (incluso C # e Java, ovviamente) c'è un Set ( HashSet in C #), che una raccolta, che può contenere al massimo un articolo da ciascun insieme di uguali le istanze.

Si possono aggiungere oggetti, rimuovere oggetti e verificare se il set contiene un oggetto. Ma perché è impossibile ottenere un particolare 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 oggetto è iterare sull'intera collezione e controllare tutti gli oggetti per l'uguaglianza. Tuttavia, questo richiede O(n) di tempo anziché O(1) !

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

Ci sono alcune domande SO correlate:

link

link

    
posta vojta 01.11.2016 - 08:58
fonte

7 risposte

66

Il problema qui non è che HashSet manchi di un metodo Get , è che il tuo codice non ha senso dal punto di vista del tipo HashSet .

Quel metodo Get è effettivamente "prendi questo valore, per favore", al quale la gente del framework .NET risponderà sensibilmente "eh? Hai già quel valore <confused face /> ".

Se desideri archiviare elementi e recuperarli in base a un altro valore leggermente diverso, utilizza Dictionary<String, MyClass> come puoi fare ora:

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

The information of equality leaks from the encapsulated class. If I wanted to change the set of properties involved in Equals, I would have to change the code outside MyClass...

Ebbene sì, ma è perché MyClass si nutre del principio del minimo stupore (POLA). Con questa 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ò, MyClass deve essere chiaramente documentato sulla sua strana forma di uguaglianza. Fatto ciò, non è più incapsulato e cambiare il modo in cui funziona l'uguaglianza romperebbe il principio aperto / chiuso. Ergo, non dovrebbe cambiare e quindi Dictionary<String, MyClass> è una buona soluzione per questo strano requisito.

    
risposta data 01.11.2016 - 09:46
fonte
24

Hai già l'elemento "presente" nel set - lo hai passato come chiave.

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

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

La struttura dei dati che stai cercando è un Dictionary<X, MyClass> in cui X ottiene in qualche modo l'As out delle MyClasses.

Il tipo di dizionario C # è bello sotto questo aspetto, in quanto consente di fornire a IEqualityComparer le chiavi.

Per l'esempio dato, 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
    
risposta data 01.11.2016 - 10:57
fonte
19

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

  • effettiva uguaglianza, dove tutti i campi sono uguali
  • imposta l'uguaglianza dei membri, dove solo A è uguale

Se si usasse la relazione di uguaglianza effettiva nell'insieme, il problema di recuperare un particolare oggetto dall'insieme non si pone - per verificare se un oggetto è nell'insieme, si dispone già di quell'oggetto. Pertanto, non è mai necessario recuperare un'istanza particolare da un set, presupponendo che si stia utilizzando la relazione di uguaglianza corretta.

Potremmo anche sostenere che un set è un tipo di dati astratti che è definito esclusivamente dalla relazione S contains x o x is-element-of S ("funzione caratteristica"). Se vuoi altre operazioni, in realtà non stai cercando un set.

Ciò che accade abbastanza spesso - ma ciò che non è un insieme - è che raggruppiamo tutti gli oggetti in distinte classi di equivalenza . Gli oggetti in ciascuna di queste classi o sottoinsiemi 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. Questo sarebbe un mapping dalla classe di equivalenza all'elemento rappresentativo.

In C #, un dizionario può usare una relazione di uguaglianza esplicita, penso. Altrimenti, tale relazione può essere implementata scrivendo una classe wrapper veloce. 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)); }
}
    
risposta data 01.11.2016 - 09:42
fonte
7

But why is it impossible to get a particular item from the set?

Perché non è a questo che servono gli insiemi.

Fammi riformulare l'esempio.

"I have a HashSet that I want store MyClass objects in and I want to be able to get them by using the property A that equals the object's property A".

Se sostituisci "HashSet" con "Collection", "objects" con "Values" e "property A" con "Key", la frase diventa:

"I have a Collection that I want to store MyClass Values in and I want to be able to get them by using the Key that equals the object's Key".

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

La risposta è che non vengono utilizzati per la stessa cosa. La ragione per utilizzare un set è garantire l'unicità dei suoi contenuti individuali, altrimenti potresti semplicemente utilizzare un elenco o un array. Il comportamento descritto nella domanda è a cosa serve un dizionario. Tutti i progettisti di linguaggi non hanno sbagliato. Non forniscono un metodo get in quanto se si ha l'oggetto ed è nell'insieme, sono equivalenti, il che significa che si otterrebbe "un oggetto equivalente". Sostenendo che HashSet dovrebbe essere implementato in modo tale da poter "ottenere" oggetti non equivalenti che hai definito uguali è un non-starter quando le lingue forniscono altre strutture dati che ti permettono di farlo.

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

    
risposta data 02.11.2016 - 02:21
fonte
6

Non appena esegui l'override equivale a un override del codice hash. Non appena hai fatto ciò, la tua "istanza" non dovrebbe mai cambiare di nuovo lo stato interno.

Se non si esegue l'override di equals e l'identità dell'oggetto VM di hashcode viene utilizzata per determinare l'uguaglianza. Se metti questo oggetto in un Set, puoi ritrovarlo di nuovo.

La modifica di un valore di un oggetto che viene utilizzato per determinare l'uguaglianza porterà alla non tracciabilità di questo oggetto in strutture basate su hash.

Quindi un incantatore su A è pericoloso.

Ora non hai B che non partecipa all'uguaglianza. Il problema qui è semanticamente non tecnicamente. Perché il cambiamento tecnico di B è neutrale rispetto all'uguaglianza. Semanticamente B deve essere qualcosa come un flag "versione".

Il punto è:

Se si hanno due oggetti uguali A ma non B, si presume che uno di questi oggetti sia più nuovo dell'altro. Se B non ha informazioni sulla versione, questa ipotesi è nascosta nell'algoritmo QUANDO si decide di "sovrascrivere / aggiornare" questo oggetto in un Set. Questo percorso del codice sorgente in cui ciò può accadere potrebbe non essere ovvio, 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 contiene informazioni sulla versione, si espone l'ipotesi che in precedenza fosse solo implicitamente derivabile dal codice. Ora puoi vedere, quell'oggetto Y è una versione più recente di X.

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

Per evitare l'operazione nascosta "sostituzione di vecchio contro 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.

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

    
risposta data 02.11.2016 - 08:04
fonte
3

Specificamente in Java, HashSet è stato inizialmente implementato utilizzando comunque HashMap e ignorando il valore. Quindi il progetto iniziale non prevedeva alcun vantaggio nel fornire un metodo get a HashSet . Se desideri archiviare e recuperare un valore canonico tra vari oggetti uguali, devi solo utilizzare HashMap .

Non mi sono tenuto aggiornato con dettagli di implementazione di questo tipo, quindi non posso dire se questo ragionamento si applica ancora completamente in Java, figuriamoci in C # ecc. Ma anche se HashSet è stato reimplementato per usare meno memoria di HashMap , in ogni caso sarebbe una brusca modifica aggiungere un nuovo metodo all'interfaccia Set . Quindi è un bel po 'di dolore per un guadagno che non tutti considerano utile.

    
risposta data 01.11.2016 - 14:21
fonte
2

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

In C ++, std::set è un insieme ordinato. Ha un metodo .find che cerca l'elemento in base all'operatore dell'ordine < o binario bool(T,T) che fornisci. Puoi utilizzare find per implementare l'operazione get desiderata.

Infatti, se la funzione bool(T,T) che hai fornito ha un contrassegno specifico su di esso ( is_transparent ), puoi passare in oggetti di un tipo diverso per il quale la funzione ha sovraccarichi. Ciò significa che non devi attaccare il secondo campo "fittizio" dei dati intomtye, ma solo assicurarti che l'operazione di ordinamento che usi possa ordinare tra la ricerca e i tipi di contenuto contenuto.

Ciò consente un efficiente:

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

dove my_string_compare comprende come ordinare interi e stringhe senza prima convertire il numero intero in una stringa (a un costo potenziale).

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

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 del set o eliminarlo, ecc.

In breve, i contenitori standard di tutte le lingue non hanno i difetti che descrivi. I contenitori basati su iteratori della libreria standard C ++ non lo fanno, e almeno alcuni dei contenitori esistevano prima di qualsiasi altra delle lingue che hai descritto e la possibilità di ottenere in modo ancora più efficiente di come descrivi ha anche stato aggiunto. Non c'è niente di sbagliato nel tuo progetto o nel volere quell'operazione; i progettisti dei Set che stai usando semplicemente non hanno fornito quell'interfaccia.

Contenitori standard C ++ in cui è stato progettato per avvolgere in modo pulito le operazioni di basso livello del codice C equivalente a mano, progettato per corrispondere al modo in cui è possibile scrivere in modo efficiente durante l'assemblaggio. I suoi iteratori sono un'astrazione di puntatori in stile C. Le lingue che hai menzionato si sono tutte spostate lontano dai puntatori come concetto, quindi non hanno utilizzato l'astrazione iteratore.

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

Il costo è che ci sono regole di invalidazione iterativa che devi tracciare e alcune operazioni richiedono 2 passaggi invece di uno (che rende più rumoroso il codice client). Il vantaggio è che la robusta astrazione consente un utilizzo più avanzato rispetto a quelli che i progettisti dell'API avevano in mente in origine.

    
risposta data 01.11.2016 - 21:47
fonte

Leggi altre domande sui tag