Czy istnieje lepszy sposób używania słowników C # niż TryGetValue?

19

Często szukam pytań w Internecie, a wiele rozwiązań obejmuje słowniki. Jednak za każdym razem, gdy próbuję je zaimplementować, otrzymuję ten okropny smród w moim kodzie. Na przykład za każdym razem, gdy chcę użyć wartości:

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

To 4 linie kodu, które zasadniczo wykonują następujące czynności: DoSomethingWith(dict["key"])

Słyszałem, że użycie słowa kluczowego out jest anty-wzorcem, ponieważ powoduje, że funkcje mutują ich parametry.

Często też potrzebuję „odwróconego” słownika, w którym zmieniam klucze i wartości.

Podobnie często chciałbym iterować elementy w słowniku i przekonać się, że przekształcam klucze lub wartości na listy itp., Aby to zrobić lepiej.

Wydaje mi się, że prawie zawsze istnieje lepszy, bardziej elegancki sposób korzystania ze słowników, ale jestem zagubiony.

Adam B.
źródło
7
Mogą istnieć inne sposoby, ale generalnie używam ContainsKey przed próbą uzyskania wartości.
Robbie Dee
2
Szczerze mówiąc, z kilkoma wyjątkami, jeśli wiesz, jakie są twoje klucze słownikowe przed czasem, prawdopodobnie istnieje lepszy sposób. Jeśli nie wiesz, klucze dyktafonu nie powinny być w ogóle zakodowane na stałe. Słowniki są przydatne do pracy z częściowo ustrukturyzowanymi obiektami lub danymi, w których pola są przynajmniej w większości prostopadłe do aplikacji. IMO, im bardziej pola te stają się istotnymi koncepcjami biznesowymi / domenowymi, tym mniej pomocne stają się słowniki do pracy z tymi koncepcjami.
svidgen
6
@RobbieDee: musisz jednak zachować ostrożność, ponieważ powoduje to wyścig. Możliwe jest usunięcie klucza między wywołaniem ContainsKey a uzyskaniem wartości.
whatsisname
12
@whatsisname: W tych warunkach bardziej odpowiedni byłby ConcurrentDictionary . Kolekcje w System.Collections.Genericprzestrzeni nazw nie są bezpieczne dla wątków .
Robert Harvey
4
Dobre 50% czasu, kiedy widzę, że ktoś używa Dictionary, tak naprawdę chcieli nowej klasy. Dla tych 50% (tylko) jest to zapach designerski.
BlueRaja - Danny Pflughoeft

Odpowiedzi:

23

Słowniki (C # lub inne) to po prostu kontener, w którym wyszukujesz wartość na podstawie klucza. W wielu językach jest on bardziej poprawnie identyfikowany jako mapa, a najczęstszą implementacją jest HashMap.

Problemem do rozważenia jest to, co dzieje się, gdy klucz nie istnieje. Niektóre języki zachowują się, zwracając nulllub nilinną równoważną wartość. Cicha domyślna wartość zamiast informowania, że ​​wartość nie istnieje.

Na dobre lub na złe projektanci bibliotek C # wymyślili idiom, aby poradzić sobie z zachowaniem. Uznali, że domyślnym sposobem wyszukiwania wartości, która nie istnieje, jest zgłoszenie wyjątku. Jeśli chcesz uniknąć wyjątków, możesz użyć Trywariantu. Jest to to samo podejście, którego używają do parsowania łańcuchów na liczbach całkowitych lub obiektach daty / godziny. Zasadniczo wpływ jest następujący:

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

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

I to przeszło do słownika, którego indeksator deleguje do Get(index):

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

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

Jest to po prostu sposób zaprojektowania języka.


Czy outnależy odradzać zmienne?

C # nie jest pierwszym językiem, który je ma i mają swój cel w określonych sytuacjach. Jeśli próbujesz zbudować wysoce współbieżny system, nie możesz używać outzmiennych na granicy współbieżności.

Na wiele sposobów, jeśli istnieje idiom, który jest popierany przez język i głównych dostawców bibliotek, staram się przyjąć te idiomy w moich interfejsach API. Dzięki temu interfejs API wydaje się bardziej spójny w domu w tym języku. Zatem metoda napisana w Ruby nie będzie wyglądać jak metoda napisana w C #, C lub Python. Każdy z nich ma preferowany sposób budowania kodu, a praca z nim pomaga użytkownikom interfejsu API nauczyć się go szybciej.


Czy mapy są ogólnie anty-wzorem?

Mają swój cel, ale wiele razy mogą być złym rozwiązaniem dla twojego celu. Szczególnie, jeśli potrzebujesz mapowania dwukierunkowego, czego potrzebujesz. Istnieje wiele kontenerów i sposobów organizowania danych. Istnieje wiele metod, z których możesz skorzystać, a czasem musisz zastanowić się, zanim wybierzesz ten pojemnik.

Jeśli masz bardzo krótką listę dwukierunkowych wartości mapowania, może być potrzebna tylko lista krotek. Lub listę struktur, w której równie łatwo możesz znaleźć pierwsze dopasowanie po obu stronach mapowania.

Pomyśl o problematycznej dziedzinie i wybierz najbardziej odpowiednie narzędzie do pracy. Jeśli nie ma, utwórz go.

Berin Loritsch
źródło
4
„wtedy może być potrzebna tylko lista krotek. Lub lista struktur, w której równie łatwo możesz znaleźć pierwsze dopasowanie po obu stronach mapowania”. - Jeśli istnieją optymalizacje dla małych zestawów, powinny być wykonane w bibliotece, a nie gumowane w kodzie użytkownika.
Blrfl
Jeśli wybierzesz listę krotek lub słownik, będzie to szczegół implementacji. Chodzi o zrozumienie swojej problematycznej domeny i użycie odpowiedniego narzędzia do pracy. Oczywiście istnieje lista i słownik. W typowym przypadku słownik jest poprawny, ale być może dla jednej lub dwóch aplikacji musisz użyć listy.
Berin Loritsch
3
Tak jest, ale jeśli zachowanie jest nadal takie samo, określenie to powinno znajdować się w bibliotece. Natknąłem się na implementacje kontenerów w innych językach, które przełączą algorytmy, jeśli powiedzą mi z góry, że liczba wpisów będzie niewielka. Zgadzam się z twoją odpowiedzią, ale kiedy się zorientujesz, powinna znajdować się w bibliotece, być może jako SmallBidirectionalMap.
Blrfl
5
„Na dobre lub na złe projektanci bibliotek C # wymyślili idiom, aby poradzić sobie z tym zachowaniem”. - Myślę, że trafiłeś w sedno: „wymyślili” idiom, zamiast używać powszechnie używanego, który jest opcjonalnym rodzajem zwrotu.
Jörg W Mittag
3
@ JörgWMittag Jeśli mówisz o czymś takim Option<T>, myślę, że znacznie utrudniłoby to użycie tej metody w C # 2.0, ponieważ nie miała ona dopasowania wzorca.
svick
21

Kilka dobrych odpowiedzi tutaj na temat ogólnych zasad hashtables / słowników. Ale pomyślałem, że dotknę twojego przykładu kodu,

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

Począwszy od C # 7 (który moim zdaniem ma około dwóch lat), można to uprościć:

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

I oczywiście można go sprowadzić do jednej linii:

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

Jeśli masz wartość domyślną, gdy klucz nie istnieje, może on wyglądać następująco:

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

Możesz więc uzyskać kompaktowe formularze, używając stosunkowo nowych dodatków językowych.

David Arno
źródło
1
Dobre wywołanie składni v7, fajna mała opcja, aby wyciąć dodatkową linię definicji, jednocześnie pozwalając na użycie var +1
BrianH
2
Zauważ też, że jeśli "key"nie jest obecny, xzostanie zainicjowanydefault(TValue)
Peter Duniho
Może być też miły jako rozszerzenie ogólne, przywołane jak"key".DoSomethingWithThis()
Ross Presser
Bardzo wolę projekty, które wykorzystują getOrElse("key", defaultValue) obiekt Null, to wciąż mój ulubiony wzór. Działaj w ten sposób, a nie obchodzi Cię, czy TryGetValuezwróci wartość prawda czy fałsz.
candied_orange
1
Wydaje mi się, że ta odpowiedź zasługuje na zastrzeżenie, że pisanie kompaktowego kodu ze względu na jego kompaktowość jest złe. Może to znacznie utrudnić odczytanie kodu. Dodatkowo, o ile dobrze pamiętam, TryGetValuenie jest bezpieczny dla atomów / wątków, więc równie łatwo można wykonać jedno sprawdzenie istnienia i drugie, aby chwycić i operować wartością
Marie,
12

Nie jest to ani zapach kodu, ani anty-wzór, ponieważ używanie funkcji w stylu TryGet z parametrem out to idiomatyczne C #. Istnieją jednak 3 opcje w języku C # do pracy ze słownikiem, więc jeśli masz pewność, że używasz odpowiedniego dla swojej sytuacji. Myślę, że wiem, skąd pochodzą pogłoski o problemie z użyciem parametru out, więc zajmę się tym na końcu.

Jakie funkcje używać podczas pracy ze słownikiem C #:

  1. Jeśli masz pewność, że klucz będzie w Słowniku, użyj właściwości Item [TKey]
  2. Jeśli klucz powinien znajdować się w Słowniku, ale jest zły / rzadki / problematyczny, że go nie ma, należy użyć Try ... Catch, aby błąd został zgłoszony, a następnie można próbować obsłużyć błąd z wdziękiem
  3. Jeśli nie jesteś pewien, czy klucz będzie w Słowniku, użyj TryGet z parametrem out

Aby to uzasadnić, wystarczy zajrzeć do dokumentacji Dictionary TryGetValue, w sekcji „Uwagi” :

Ta metoda łączy funkcjonalność metody ContainsKey i właściwości Item [TKey].

...

Użyj metody TryGetValue, jeśli kod często próbuje uzyskać dostęp do kluczy, których nie ma w słowniku. Korzystanie z tej metody jest bardziej wydajne niż wyłapywanie wyjątku KeyNotFoundException zgłaszanego przez właściwość Item [TKey].

Ta metoda zbliża się do operacji O (1).

Głównym powodem, dla którego istnieje TryGetValue, jest działanie jako wygodniejszy sposób korzystania z ContainsKey i Item [TKey], przy jednoczesnym unikaniu konieczności dwukrotnego przeszukiwania słownika - więc udawanie, że nie istnieje i wykonywanie tych dwóch czynności ręcznie jest raczej niezręczne wybór.

W praktyce rzadko korzystam z surowego słownika, z powodu tej prostej maksymy: wybierz najbardziej ogólną klasę / kontener, która zapewnia potrzebną funkcjonalność . Słownik nie został zaprojektowany tak, aby wyszukiwać według wartości, a nie klucza (na przykład), więc jeśli jest to coś, czego chcesz, sensowniejsze może być użycie alternatywnej struktury. Wydaje mi się, że kiedyś użyłem Słownika w ostatnim rocznym projekcie programistycznym, po prostu dlatego, że rzadko było to właściwe narzędzie do pracy, którą próbowałem wykonać. Słownik z pewnością nie jest szwajcarskim scyzorykiem z zestawu narzędzi C #.

Co jest nie tak z naszymi parametrami?

CA1021: Unikaj parametrów

Mimo że zwracane wartości są powszechne i często stosowane, prawidłowe stosowanie parametrów wyjściowych i referencyjnych wymaga pośrednich umiejętności projektowania i kodowania. Architekci bibliotek, którzy projektują dla szerokiej publiczności, nie powinni oczekiwać, że użytkownicy opanują pracę z parametrami out lub ref.

Zgaduję, że właśnie tam usłyszałeś, że nasze parametry były czymś w rodzaju anty-wzorca. Podobnie jak w przypadku wszystkich reguł, powinieneś przeczytać bliżej, aby zrozumieć „dlaczego”, aw tym przypadku jest nawet wyraźna wzmianka o tym, jak wzorzec Try nie narusza reguły :

Metody implementujące wzorzec Try, takie jak System.Int32.TryParse, nie zgłaszają tego naruszenia.

BrianH
źródło
Całą tę listę wskazówek chciałbym przeczytać więcej osób. Dla tych, którzy podążają, oto jego podstawa. docs.microsoft.com/en-us/visualstudio/code-quality/…
Peter Wone
10

W słownikach C # brakuje co najmniej dwóch metod, które moim zdaniem znacznie oczyszczają kod w wielu sytuacjach w innych językach. Pierwszy zwraca an Option, który pozwala pisać kod w Scali w następujący sposób:

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

Drugi zwraca wartość domyślną określoną przez użytkownika, jeśli klucz nie zostanie znaleziony:

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

Jest coś do powiedzenia na temat używania idiomów, które zapewnia język, gdy jest to właściwe, na przykład Trywzorca, ale to nie znaczy, że musisz używać tylko tego, co zapewnia język. Jesteśmy programistami. Można tworzyć nowe abstrakcje, aby ułatwić zrozumienie naszej konkretnej sytuacji, szczególnie jeśli eliminuje to wiele powtórzeń. Jeśli często potrzebujesz czegoś, na przykład wyszukiwania wstecznego lub iteracji po wartościach, zrób to. Utwórz interfejs, który chcesz mieć.

Karl Bielefeldt
źródło
Podoba mi się druga opcja. Może będę musiał napisać metodę rozszerzenia lub dwie :)
Adam B
2
po stronie plus metody rozszerzenia c # oznaczają, że możesz je zaimplementować samodzielnie
jk.
5

To 4 linie kodu, które zasadniczo wykonują następujące czynności: DoSomethingWith(dict["key"])

Zgadzam się, że to nieeleganckie. Mechanizm, który lubię używać w tym przypadku, w którym wartość jest typem struktury, to:

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

Teraz mamy nową wersję TryGetValuetego powrotu int?. Następnie możemy wykonać podobną sztuczkę w celu przedłużenia T?:

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

A teraz złóż to razem:

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

i sprowadzamy się do jednego, jasnego stwierdzenia.

Słyszałem, że użycie słowa kluczowego out jest anty-wzorcem, ponieważ powoduje, że funkcje mutują ich parametry.

Byłbym nieco słabiej sformułowany i powiedziałbym, że unikanie mutacji, gdy jest to możliwe, jest dobrym pomysłem.

Często potrzebuję „odwróconego” słownika, w którym zmieniam klucze i wartości.

Następnie zaimplementuj lub uzyskaj słownik dwukierunkowy. Można je łatwo napisać lub w Internecie dostępnych jest wiele wdrożeń. Istnieje tutaj wiele implementacji, na przykład:

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

Podobnie często chciałbym iterować elementy w słowniku i przekonać się, że przekształcam klucze lub wartości na listy itp., Aby to zrobić lepiej.

Jasne, że wszyscy.

Wydaje mi się, że prawie zawsze istnieje lepszy, bardziej elegancki sposób korzystania ze słowników, ale jestem zagubiony.

Zadaj sobie pytanie „przypuśćmy, że miałem klasę inną niż ta, Dictionaryktóra zaimplementowała dokładnie tę operację, którą chciałbym wykonać; jak wyglądałaby ta klasa?” Następnie, po udzieleniu odpowiedzi na to pytanie, zaimplementuj tę klasę . Jesteś programistą komputerowym. Rozwiąż swoje problemy pisząc programy komputerowe!

Eric Lippert
źródło
Dzięki. Czy sądzisz, że mógłbyś dowiedzieć się więcej o tym, co faktycznie robi metoda działania? Jestem nowy w działaniach.
Adam B
1
@AdamB akcja to obiekt reprezentujący możliwość wywołania metody void. Jak widzisz, po stronie wywołującej przekazujemy nazwę metody void, której lista parametrów odpowiada argumentom typu ogólnego akcji. Po stronie wywołującej akcja jest wywoływana jak każda inna metoda.
Eric Lippert,
1
@AdamB: Możesz myśleć, że Action<T>jest bardzo podobny interface IAction<T> { void Invoke(T t); }, ale z bardzo łagodnymi regułami dotyczącymi tego, co „implementuje” ten interfejs i jak Invokemożna go wywoływać. Jeśli chcesz dowiedzieć się więcej, dowiedz się o „delegatach” w C #, a następnie dowiedz się o wyrażeniach lambda.
Eric Lippert,
Ok, chyba mam to. Tak więc akcja pozwala wstawić metodę jako parametr DoIt (). I wywołuje tę metodę.
Adam B
@AdamB: Dokładnie tak. Jest to przykład „programowania funkcjonalnego z funkcjami wyższego rzędu”. Większość funkcji przyjmuje dane jako argumenty; funkcje wyższego rzędu przyjmują funkcje za argumenty, a następnie robią z nimi funkcje. Gdy dowiesz się więcej o C #, zobaczysz, że LINQ jest całkowicie zaimplementowany z funkcjami wyższego rzędu.
Eric Lippert,
4

TryGetValue()Konstrukt jest konieczne tylko wtedy, jeśli nie wiem, czy „klucz” jest obecny jako klucz w słowniku, czy nie, w przeciwnym razie DoSomethingWith(dict["key"])jest całkowicie poprawny.

ContainsKey()Zamiast tego „mniej brudnym” podejściem może być sprawdzenie.

Wędrowny
źródło
6
„Mniej„ brudnym ”podejściem może być zamiast tego użycie ContainsKey () jako czeku.” Nie zgadzam się. Jakkolwiek nieoptymalne TryGetValue, przynajmniej utrudnia zapomnienie o obsłudze pustej skrzynki. Idealnie chciałbym, aby to po prostu zwróciło Opcjonalne.
Alexander - Przywróć Monikę
1
Istnieje potencjalny problem z ContainsKeypodejściem do aplikacji wielowątkowych, czyli luki w zabezpieczeniach od czasu sprawdzenia do momentu użycia (TOCTOU). Co się stanie, jeśli jakiś inny wątek usunie klucz między połączeniami do ContainsKeyi GetValue?
David Hammen
2
@JAD Komponenty opcjonalne komponują. Możesz miećOptional<Optional<T>>
Aleksandra - Przywróć Monikę
2
@DavidHammen Żeby było jasne, że trzeba TryGetValuena ConcurrentDictionary. Zwykły słownik nie został zsynchronizowany
Alexander - Przywróć Monikę
2
@JAD Nie, (opcjonalny typ Javy wieje, nie zaczynaj mnie). Mówię bardziej abstrakcyjnie
Alexander - Przywróć Monikę
2

Inne odpowiedzi zawierają świetne punkty, więc nie będę ich tutaj powtarzał, ale skupię się na tej części, która do tej pory wydaje się w dużej mierze ignorowana:

Podobnie często chciałbym iterować elementy w słowniku i przekonać się, że przekształcam klucze lub wartości na listy itp., Aby to zrobić lepiej.

W rzeczywistości iteracja słownika jest dość prosta, ponieważ implementuje on IEnumerable:

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

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

Jeśli wolisz Linq, to ​​też działa dobrze:

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

Podsumowując, nie uważam Słowników za antypattern - są one tylko konkretnym narzędziem o określonych zastosowaniach.

Ponadto, aby uzyskać jeszcze więcej szczegółów, sprawdź SortedDictionary(używa drzew RB dla bardziej przewidywalnej wydajności) i SortedList(który jest także myląco nazwanym słownikiem, który poświęca szybkość wstawiania dla prędkości wyszukiwania, ale świeci, jeśli gapisz się na ustalone, wstępne posortowany zestaw). Miałem przypadek, w którym wymieniasz Dictionaryz SortedDictionaryzaowocowało rząd wielkości szybciej wykonanie (ale może się zdarzyć w drugą stronę też).

Vilx-
źródło
1

Jeśli uważasz, że używanie słownika jest niewygodne, może nie być dobrym wyborem dla twojego problemu. Słowniki są świetne, ale jak zauważył jeden z komentatorów, często są używane jako skrót do czegoś, co powinno być klasą. Albo sam słownik może być właściwy jako podstawowa metoda przechowywania, ale powinna być wokół niego klasa opakowania, aby zapewnić pożądane metody obsługi.

Wiele powiedziano o Dict [klucz] kontra TryGet. Często używam iteracji w słowniku za pomocą KeyValuePair. Najwyraźniej jest to mniej znany konstrukt.

Główną zaletą słownika jest to, że jest on naprawdę szybki w porównaniu do innych kolekcji wraz ze wzrostem liczby przedmiotów. Jeśli musisz sprawdzić, czy klucz istnieje, możesz zadać sobie pytanie, czy użycie słownika jest właściwe. Jako klient zazwyczaj powinieneś wiedzieć, co tam umieścisz, a tym samym, co można bezpiecznie zapytać.

Martin Maat
źródło