Esiste un modo migliore per usare i dizionari C # di TryGetValue?


19

Mi trovo spesso a cercare domande online e molte soluzioni includono dizionari. Tuttavia, ogni volta che provo a implementarli, ottengo questo orribile odore nel mio codice. Ad esempio ogni volta che voglio usare un valore:

int x;
if (dict.TryGetValue("key", out x)) {
    DoSomethingWith(x);
}

Sono 4 righe di codice per eseguire essenzialmente le seguenti operazioni: DoSomethingWith(dict["key"])

Ho sentito che usare la parola chiave out è un anti pattern perché rende le funzioni mutanti i loro parametri.

Inoltre, mi trovo spesso a necessitare di un dizionario "invertito", in cui capovolgo chiavi e valori.

Allo stesso modo, vorrei spesso scorrere gli elementi in un dizionario e ritrovarmi a convertire chiavi o valori in elenchi ecc. Per fare meglio.

Sento che c'è quasi sempre un modo migliore, più elegante di usare i dizionari, ma sono in perdita.


7
Potrebbero esistere altri modi, ma generalmente utilizzo un ContainsKey prima di cercare di ottenere un valore.
Robbie Dee,

2
Onestamente, con poche eccezioni, se sai quali sono le tue chiavi del dizionario in anticipo, probabilmente c'è un modo migliore. Se non lo sai, le chiavi dict non dovrebbero in genere essere codificate. I dizionari sono utili per lavorare con oggetti o dati semi-strutturati in cui i campi sono almeno per lo più ortogonali all'applicazione. IMO, più questi campi diventano vicini a concetti aziendali / di dominio pertinenti, meno dizionari diventano utili per lavorare con questi concetti.
svidgen,

6
@RobbieDee: devi fare attenzione quando lo fai, in quanto crea una condizione di gara. È possibile che la chiave possa essere rimossa tra la chiamata a ContainsKey e l'ottenimento del valore.
whatsisname

12
@whatsisname: in queste condizioni, un ConcurrentDictionary sarebbe più adatto. Le raccolte nello System.Collections.Genericspazio dei nomi non sono thread-safe .
Robert Harvey,

4
Un buon 50% delle volte vedo qualcuno che usa un Dictionary, quello che volevano davvero era una nuova classe. Per quel 50% (solo), è un odore di design.
BlueRaja - Danny Pflughoeft,

Risposte:


23

I dizionari (C # o altro) sono semplicemente un contenitore in cui si cerca un valore basato su una chiave. In molte lingue è identificato più correttamente come una mappa, l'implementazione più comune è una HashMap.

Il problema da considerare è ciò che accade quando non esiste una chiave. Alcune lingue si comportano restituendo nullo nilo altri valori equivalenti. Silenziosamente il default su un valore invece di informarti che un valore non esiste.

Nel bene e nel male, i progettisti della biblioteca C # hanno escogitato un linguaggio per affrontare il comportamento. Hanno sostenuto che il comportamento predefinito per la ricerca di un valore che non esiste è quello di generare un'eccezione. Se si desidera evitare eccezioni, è possibile utilizzare la Tryvariante. È lo stesso approccio che usano per analizzare le stringhe in numeri interi o oggetti data / ora. In sostanza, l'impatto è così:

T count = int.Parse("12T45"); // throws exception

if (int.TryParse("12T45", out count))
{
    // Does not throw exception
}

E quello portato avanti nel dizionario, il cui indicizzatore delega a Get(index):

var myvalue = dict["12345"]; // throws exception
myvalue = dict.Get("12345"); // throws exception

if (dict.TryGet("12345", out myvalue))
{
    // Does not throw exception
}

Questo è semplicemente il modo in cui è progettata la lingua.


Le outvariabili dovrebbero essere scoraggiate?

C # non è la prima lingua ad averli e hanno il loro scopo in situazioni specifiche. Se si sta tentando di creare un sistema altamente concorrente, non è possibile utilizzare le outvariabili ai limiti della concorrenza.

In molti modi, se esiste un idioma che è sposato dal fornitore di servizi linguistici e di librerie di base, provo ad adottare quegli idiomi nelle mie API. Ciò rende l'API più coerente e a suo agio in quella lingua. Quindi un metodo scritto in Ruby non sembrerà un metodo scritto in C #, C o Python. Ognuno di essi ha un modo preferito di costruire codice e lavorare con questo aiuta gli utenti dell'API a impararlo più rapidamente.


Le mappe in generale sono anti-pattern?

Hanno il loro scopo, ma molte volte potrebbero essere la soluzione sbagliata per lo scopo che hai. Soprattutto se hai una mappatura bidirezionale di cui hai bisogno. Esistono molti contenitori e modi per organizzare i dati. Esistono molti approcci che è possibile utilizzare e talvolta è necessario pensarci un po 'prima di scegliere quel contenitore.

Se hai un elenco molto breve di valori di mappatura bidirezionali, potresti aver bisogno solo di un elenco di tuple. O un elenco di strutture, in cui è possibile trovare facilmente la prima corrispondenza su entrambi i lati della mappatura.

Pensa al dominio problematico e scegli lo strumento più appropriato per il lavoro. Se non ce n'è uno, crealo.


4
"allora potresti aver bisogno solo di un elenco di tuple. O di un elenco di strutture, in cui puoi trovare altrettanto facilmente la prima corrispondenza su entrambi i lati della mappatura." - Se ci sono ottimizzazioni per piccoli set, questi dovrebbero essere fatti nella libreria, non timbrati nel codice utente.
Blrfl,

Se scegli un elenco di tuple o un dizionario, questo è un dettaglio di implementazione. Si tratta di comprendere il dominio del problema e di utilizzare lo strumento giusto per il lavoro. Ovviamente esiste un elenco ed esiste un dizionario. Nel caso comune, il dizionario è corretto, ma forse per una o due applicazioni è necessario utilizzare l'elenco.
Berin Loritsch,

3
Lo è, ma se il comportamento è sempre lo stesso, tale determinazione dovrebbe essere nella libreria. Ho incontrato implementazioni di container in altre lingue che cambieranno algoritmi se detto a priori che il numero di voci sarà piccolo. Sono d'accordo con la tua risposta, ma una volta che questo è stato capito, dovrebbe essere in una libreria, forse come SmallBidirectionalMap.
Blrfl,

5
"Nel bene e nel male, i progettisti delle biblioteche C # hanno escogitato un linguaggio per affrontare il comportamento." - Penso che tu abbia colpito l'unghia sulla testa: hanno "escogitato" un idioma, invece di usarne uno esistente ampiamente usato, che è un tipo di ritorno opzionale.
Jörg W Mittag,

3
@ JörgWMittag Se stai parlando di qualcosa del genere Option<T>, penso che renderebbe il metodo molto più difficile da usare in C # 2.0, perché non aveva un pattern matching.
svick,

21

Alcune buone risposte qui sui principi generali di hashtables / dizionari. Ma ho pensato di toccare il tuo esempio di codice,

int x;
if (dict.TryGetValue("key", out x)) 
{
    DoSomethingWith(x);
}

A partire da C # 7 (che credo abbia circa due anni), ciò può essere semplificato per:

if (dict.TryGetValue("key", out var x))
{
    DoSomethingWith(x);
}

E ovviamente potrebbe essere ridotto a una riga:

if (dict.TryGetValue("key", out var x)) DoSomethingWith(x);

Se hai un valore predefinito per quando la chiave non esiste, può diventare:

DoSomethingWith(dict.TryGetValue("key", out var x) ? x : defaultValue);

In questo modo puoi ottenere moduli compatti utilizzando aggiunte linguistiche ragionevolmente recenti.


1
Buona chiamata sulla sintassi v7, bella piccola opzione per dover tagliare la linea di definizione aggiuntiva consentendo l'uso di var +1
BrianH,

2
Si noti inoltre che se "key"non è presente, xverrà inizializzato adefault(TValue)
Peter Duniho il

Potrebbe essere carino anche come estensione generica, invocata come"key".DoSomethingWithThis()
Ross Presser,

Preferisco di gran lunga i disegni che usano l' getOrElse("key", defaultValue) oggetto Null è ancora il mio modello preferito. Funziona in questo modo e non ti importa se TryGetValuerestituisce vero o falso.
candied_orange

1
Sento che questa risposta merita una dichiarazione di non responsabilità che scrivere codice compatto per il gusto di essere compatto è un male. Può rendere il tuo codice molto più difficile da leggere. Inoltre, se ricordo bene, TryGetValuenon è atomico / thread sicuro, quindi potresti facilmente eseguire un controllo dell'esistenza e un altro per afferrare e operare sul valore
Marie,

12

Questo non è né un odore di codice né un anti-pattern, poiché l'utilizzo di funzioni in stile TryGet con un parametro out è idiomatico C #. Tuttavia, ci sono 3 opzioni fornite in C # per lavorare con un dizionario, quindi dovresti essere sicuro di utilizzare quello corretto per la tua situazione. Penso di sapere da dove viene la voce che ci sia un problema nell'uso di un parametro out, quindi alla fine lo gestirò.

Quali funzioni usare quando si lavora con un dizionario C #:

  1. Se sei sicuro che la chiave sarà nel dizionario, usa la proprietà Item [TKey]
  2. Se la chiave dovrebbe essere generalmente nel dizionario, ma è male / raro / problematico che non sia presente, dovresti usare Try ... Catch in modo che venga generato un errore e quindi puoi provare a gestire l'errore con grazia
  3. Se non si è sicuri che la chiave si troverà nel dizionario, utilizzare TryGet con il parametro out

Per giustificare ciò, è sufficiente fare riferimento alla documentazione per Dictionary TryGetValue, in "Osservazioni" :

Questo metodo combina la funzionalità del metodo ContainsKey e la proprietà Item [TKey].

...

Utilizzare il metodo TryGetValue se il codice tenta frequentemente di accedere a chiavi che non sono presenti nel dizionario. L'uso di questo metodo è più efficiente rispetto alla cattura di KeyNotFoundException generata dalla proprietà Item [TKey].

Questo metodo si avvicina a un'operazione O (1).

L'intera ragione per cui TryGetValue esiste è quella di agire come un modo più conveniente per usare ContainsKey e Item [TKey], evitando di dover cercare due volte nel dizionario - quindi fingere che non esista e fare le due cose che fa manualmente è piuttosto imbarazzante scelta.

In pratica, raramente ho mai usato un dizionario non elaborato, grazie a questa semplice massima: scegli la classe / contenitore più generico che ti offre le funzionalità di cui hai bisogno . Il dizionario non è stato progettato per cercare in base al valore piuttosto che alla chiave (ad esempio), quindi se è qualcosa che desideri potrebbe avere più senso usare una struttura alternativa. Penso di aver usato Dizionario una volta nel progetto di sviluppo dell'ultimo anno che ho fatto, semplicemente perché raramente era lo strumento giusto per il lavoro che stavo cercando di fare. Il dizionario non è certamente il coltellino svizzero della cassetta degli attrezzi C #.

Cosa c'è che non va nei nostri parametri?

CA1021: Evitare parametri fuori

Sebbene i valori di ritorno siano all'ordine del giorno e ampiamente utilizzati, la corretta applicazione dei parametri out e ref richiede competenze di progettazione e codifica intermedie. Gli architetti delle biblioteche che progettano per un pubblico generico non dovrebbero aspettarsi che gli utenti padroneggino lavorando con parametri out o ref.

Immagino che sia lì che hai sentito che i parametri erano qualcosa di simile a un anti-pattern. Come per tutte le regole, dovresti leggere più da vicino per capire il "perché", e in questo caso c'è anche una menzione esplicita di come il modello Try non viola la regola :

I metodi che implementano il modello Try, come System.Int32.TryParse, non generano questa violazione.


L'intero elenco di indicazioni è qualcosa che vorrei che più persone leggessero. Per coloro che seguono, ecco la radice di esso. docs.microsoft.com/en-us/visualstudio/code-quality/…
Peter Wone,

10

Ci sono almeno due metodi mancanti nei dizionari C # che secondo me ripuliscono considerevolmente il codice in molte situazioni in altre lingue. Il primo sta restituendo an Option, che ti permette di scrivere codice come il seguente in Scala:

dict.get("key").map(doSomethingWith)

Il secondo restituisce un valore predefinito specificato dall'utente se la chiave non viene trovata:

doSomethingWith(dict.getOrElse("key", "key not found"))

C'è qualcosa da dire per usare gli idiomi che una lingua fornisce quando appropriato, come lo Tryschema, ma ciò non significa che devi usare solo ciò che la lingua fornisce. Siamo programmatori. Va bene creare nuove astrazioni per facilitare la comprensione della nostra situazione specifica, soprattutto se elimina molte ripetizioni. Se hai spesso bisogno di qualcosa, come ricerche inverse o iterazione di valori, fallo accadere. Crea l'interfaccia che desideri avere.


Mi piace la seconda opzione. Forse dovrò scrivere un metodo di estensione o due :)
Adam B

2
sul lato positivo i metodi di estensione c # significano che puoi implementarli tu stesso
jk.

5

Sono 4 righe di codice per eseguire essenzialmente le seguenti operazioni: DoSomethingWith(dict["key"])

Sono d'accordo che questo non è elegante. Un meccanismo che mi piace usare in questo caso, in cui il valore è un tipo di struttura, è:

public static V? TryGetValue<K, V>(
      this Dictionary<K, V> dict, K key) where V : struct => 
  dict.TryGetValue(key, out V v)) ? new V?(v) : new V?();

Ora abbiamo una nuova versione di TryGetValueciò che ritorna int?. Possiamo quindi fare un trucco simile per estendere T?:

public static void DoIt<T>(
      this T? item, Action<T> action) where T : struct
{
  if (item != null) action(item.GetValueOrDefault());
}

E ora mettilo insieme:

dict.TryGetValue("key").DoIt(DoSomethingWith);

e passiamo a una singola, chiara affermazione.

Ho sentito che usare la parola chiave out è un anti pattern perché rende le funzioni mutanti i loro parametri.

Vorrei essere leggermente meno fortemente formulato e dire che evitare le mutazioni quando possibile è una buona idea.

Mi trovo spesso a necessitare di un dizionario "invertito", in cui capovolgo chiavi e valori.

Quindi implementare o ottenere un dizionario bidirezionale. Sono semplici da scrivere o ci sono molte implementazioni disponibili su Internet. Ci sono un sacco di implementazioni qui, ad esempio:

/programming/268321/bidirectional-1-to-1-dictionary-in-c-sharp

Allo stesso modo, vorrei spesso scorrere gli elementi in un dizionario e ritrovarmi a convertire chiavi o valori in elenchi ecc. Per fare meglio.

Certo, lo facciamo tutti.

Sento che c'è quasi sempre un modo migliore, più elegante di usare i dizionari, ma sono in perdita.

Chiediti "supponiamo che io abbia tenuto una classe diversa da Dictionaryquella implementata esattamente l'operazione che desidero fare; che aspetto avrebbe quella classe?" Quindi, una volta che hai risposto a quella domanda, implementa quella classe . Sei un programmatore di computer. Risolvi i tuoi problemi scrivendo programmi per computer!


Grazie. Pensi di poter dare qualche spiegazione in più su ciò che sta effettivamente facendo il metodo di azione? Sono nuovo alle azioni.
Adam B,

1
@AdamB un'azione è un oggetto che rappresenta la capacità di chiamare un metodo vuoto. Come vedi, sul lato del chiamante passiamo il nome di un metodo void il cui elenco di parametri corrisponde agli argomenti di tipo generico dell'azione. Sul lato chiamante l'azione viene invocata come qualsiasi altro metodo.
Eric Lippert,

1
@AdamB: Puoi pensare di Action<T>essere molto simile interface IAction<T> { void Invoke(T t); }, ma con regole molto indulgenti su ciò che "implementa" quell'interfaccia e su come Invokepuò essere invocato. Se vuoi saperne di più, scopri i "delegati" in C # e poi le espressioni lambda.
Eric Lippert,

Ok, penso di averlo capito. Quindi, l'azione consente di inserire un metodo come parametro per DoIt (). E chiama quel metodo.
Adam B,

@AdamB: esattamente. Questo è un esempio di "programmazione funzionale con funzioni di ordine superiore". La maggior parte delle funzioni prende i dati come argomenti; Le funzioni di ordine superiore prendono funzioni come argomenti e quindi fanno cose con quelle funzioni. Man mano che impari di più su C #, vedrai che LINQ è interamente implementato con funzioni di ordine superiore.
Eric Lippert,

4

Il TryGetValue()costrutto è necessario solo se non sai se "chiave" è presente come chiave nel dizionario o no, altrimenti DoSomethingWith(dict["key"])è perfettamente valida.

Un approccio "meno sporco" potrebbe essere invece quello di utilizzare ContainsKey()come controllo.


6
"Un approccio" meno sporco "potrebbe essere invece utilizzare ContainsKey () come controllo." Non sono d'accordo. Per quanto non sia ottimale TryGetValue, almeno rende difficile dimenticare di gestire il caso vuoto. Idealmente, vorrei che questo restituisse solo un Opzionale.
Alexander - Ripristina Monica il

1
Esiste un potenziale problema con l' ContainsKeyapproccio per le applicazioni multithread, ovvero la vulnerabilità da momento del controllo al tempo di utilizzo (TOCTOU). E se qualche altro thread cancella la chiave tra le chiamate a ContainsKeye GetValue?
David Hammen,

2
@JAD Optionals compongono. Puoi avere unOptional<Optional<T>>
Alexander - Ripristina Monica il

2
@DavidHammen Per essere chiari, si avrebbe bisogno TryGetValuein ConcurrentDictionary. Il dizionario normale non sarebbe sincronizzato
Alexander - Ripristina Monica il

2
@JAD No, (il tipo opzionale di Java soffia, non farmi iniziare). Sto parlando in modo più astratto
Alexander - Ripristina Monica il

2

Altre risposte contengono grandi punti, quindi non le ribadirò qui, ma mi concentrerò su questa parte, che finora sembra essere ampiamente ignorata:

Allo stesso modo, vorrei spesso scorrere gli elementi in un dizionario e ritrovarmi a convertire chiavi o valori in elenchi ecc. Per fare meglio.

In realtà, è abbastanza facile iterare su un dizionario, poiché implementa IEnumerable:

var dict = new Dictionary<int, string>();

foreach ( var item in dict ) {
    Console.WriteLine("{0} => {1}", item.Key, item.Value);
}

Se preferisci Linq, anche quello funziona bene:

dict.Select(x=>x.Value).WhateverElseYouWant();

Tutto sommato, non trovo i dizionari un antipasto: sono solo uno strumento specifico con usi specifici.

Inoltre, per dettagli ancora più specifici, controlla SortedDictionary(usa gli alberi RB per prestazioni più prevedibili) e SortedList(che è anche un dizionario dal nome confuso che sacrifica la velocità di inserimento per la velocità di ricerca, ma brilla se stai fissando con un pre fisso, pre set ordinato). Ho avuto un caso in cui la sostituzione di a Dictionarycon un SortedDictionaryrisultato in un ordine di grandezza esecuzione più veloce (ma potrebbe accadere anche al contrario).


1

Se ritieni che l'uso di un dizionario sia imbarazzante, potrebbe non essere la scelta giusta per il tuo problema. I dizionari sono fantastici ma, come notato da un commentatore, spesso sono usati come scorciatoia per qualcosa che avrebbe dovuto essere una classe. Oppure il dizionario stesso potrebbe essere giusto come metodo di archiviazione principale, ma dovrebbe esserci una classe wrapper attorno a esso per fornire i metodi di servizio desiderati.

Molto è stato detto su Dict [chiave] contro TryGet. Quello che uso molto è iterare sul dizionario usando KeyValuePair. Apparentemente questo è un costrutto meno comunemente noto.

Il vantaggio principale di un dizionario è che è molto veloce rispetto ad altre raccolte con l'aumentare del numero di elementi. Se devi controllare se esiste una chiave molto, potresti chiederti se il tuo uso di un dizionario è appropriato. Come cliente dovresti in genere sapere cosa hai inserito e quindi cosa sarebbe sicuro interrogare.

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.