Чи є кращий спосіб використання словників C #, ніж TryGetValue?


19

Мені здається, що я часто шукаю запитання в Інтернеті, і багато рішень включають словники. Однак кожного разу, коли я намагаюся їх реалізувати, я отримую цей жахливий реек у своєму коді. Наприклад, кожен раз, коли я хочу використовувати значення:

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

Це 4 рядки коду, щоб по суті зробити наступне: DoSomethingWith(dict["key"])

Я чув, що використання ключового слова out - це антидіапазон, оскільки він змушує функції мутувати свої параметри.

Крім того, мені дуже часто потрібен "зворотний" словник, де я гортаю клавіші та значення.

Так само я часто хотів би переглядати елементи словника і перетворювати ключі або значення в списки тощо, щоб зробити це краще.

Я відчуваю, що майже завжди є кращий, більш елегантний спосіб використання словників, але я в збитку.


7
Можуть існувати й інші способи, але я, як правило, спочатку використовую ContainsKey, перш ніж намагатися отримати значення.
Роббі Ді

2
Чесно кажучи, за кількома винятками, якщо ви знаєте, які ваші словникові ключі випереджають час, мабуть, є кращий спосіб. Якщо ви не знаєте, клавіші dict взагалі не повинні бути жорстко кодованими. Словники корисні для роботи з полуструктурированного об'єктами або даними , де поля є , по крайней мере в основному перпендикулярно до застосування. ІМО, чим ближче ці поля доречні для бізнесу / доменних понять, тим менш корисні словники стають для роботи з цими поняттями.
svidgen

6
@RobbieDee: ти повинен бути обережним, коли ти це робиш, оскільки це створює перегони. Можливо, ключ може бути видалений між викликом ContainsKey та отриманням значення.
whatsisname

12
@whatsisname: За таких умов ConcurrentDictionary було б більш підходящим. Колекції в System.Collections.Genericпросторі імен не є безпечними для потоків .
Роберт Харві

4
Хороші 50% часу я бачу, що хтось використовує Dictionary, те, що вони дійсно хотіли, це новий клас. Для тих 50% (лише) - це дизайнерський запах.
BlueRaja - Danny Pflughoeft

Відповіді:


23

Словники (C # або інше) - це просто контейнер, де ви шукаєте значення на основі ключа. У багатьох мовах його більш правильно ідентифікують як карту, найпоширенішою реалізацією якої є HashMap.

Проблема, яку слід врахувати, - це те, що відбувається, коли ключ не існує. Деякі мови поводяться шляхом повернення nullабо nilіншого еквівалентного значення. Мовчати дефолт до значення, а не повідомляти про те, що значення не існує.

На краще чи на гірше, дизайнери бібліотек C # придумали ідіому, щоб розібратися з поведінкою. Вони аргументували, що поведінка за замовчуванням для пошуку значення, яке не існує, - це викинути виняток. Якщо ви хочете уникнути винятків, тоді можете скористатися Tryваріантом. Це той самий підхід, який вони використовують для розбору рядків у цілі числа чи об'єкти дати / часу. По суті, вплив такий:

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

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

І це перейшло до словника, індексатор якого делегує Get(index):

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

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

Це просто так, як розроблена мова.


Чи outслід відмовляти змінним?

C # - це не перша мова, яка має їх, і вони мають своє призначення в конкретних ситуаціях. Якщо ви намагаєтеся побудувати високосполучну систему, то ви не можете використовувати outзмінні на межах одночасності.

Багато в чому, якщо є ідіома, яку підтримують мова та основні постачальники бібліотек, я намагаюся прийняти ці ідіоми у своїх API. Завдяки цьому API відчуває себе більш послідовно та вдома на цій мові. Таким чином, метод, написаний в Ruby, не буде схожий на метод, написаний на C #, C або Python. Кожен з них має вподобаний спосіб побудови коду, а робота з цим допомагає користувачам вашого API швидше засвоїти його.


Чи загалом Карти є антидіаграмою?

Вони мають своє призначення, але багато разів вони можуть бути неправильним рішенням для вашої мети. Особливо, якщо вам потрібне двонаправлене картографування. Існує багато контейнерів і способів організації даних. Ви можете скористатися багатьма підходами, і іноді вам потрібно трохи подумати, перш ніж вибирати контейнер.

Якщо у вас дуже короткий список значень двонаправленого відображення, вам може знадобитися лише список кортежів. Або список структур, де ви можете так само легко знайти перший матч по обидва боки карти.

Подумайте про проблемну область та виберіть найбільш відповідний інструмент для роботи. Якщо такого немає, то створіть його.


4
"тоді вам може знадобитися лише список кортежів. Або список структур, де ви можете так само легко знайти першу відповідність з обох сторін відображення." - Якщо є оптимізації для невеликих наборів, вони повинні бути зроблені в бібліотеці, а не з гумовим штампом у коді користувача.
Blrfl

Якщо ви вибрали список кортежів або словник, це детальна інформація про реалізацію. Це питання розуміння вашої проблемної області та використання правильного інструменту для роботи. Очевидно, існує список і словник існує. Для звичайного випадку словник правильний, але, можливо, для одного або двох додатків вам потрібно використовувати список.
Берін Лорич

3
Так є, але якщо поведінка все-таки однакова, це визначення має бути в бібліотеці. Я натрапив на реалізацію контейнерів іншими мовами, які будуть перемикати алгоритми, якщо апріорі сказали, що кількість записів буде невеликою. Я погоджуюся з вашою відповіддю, але як тільки це з’ясувалося, це має бути в бібліотеці, можливо, як SmallBidirectionalMap.
Blrfl

5
"На краще або на гірше, дизайнери бібліотек C # придумали ідіому, щоб розібратися з поведінкою." - Думаю, ви вдарили цвяхом по голові: вони "придумали" ідіому, замість того, щоб використовувати існуючу широко використовувану, яка є необов'язковим типом повернення.
Йорг W Міттаг

3
@ JörgWMittag Якщо ви говорите про щось подібне Option<T>, я думаю, що це зробить метод набагато важчим у використанні в C # 2.0, оскільки він не мав відповідності шаблону.
svick

21

Тут є кілька хороших відповідей щодо загальних принципів хештелів / словників. Але я думав, що торкнусь прикладу вашого коду,

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

Що стосується C # 7 (на мою думку, близько двох років), це можна спростити до:

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

І звичайно, це може бути скорочено до одного рядка:

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

Якщо у вас є значення за замовчуванням, коли ключ не існує, він може стати:

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

Таким чином, ви можете досягти компактних форм, використовуючи досить сучасні мовні доповнення.


1
Хороший дзвінок у синтаксисі v7, приємний маленький варіант, щоб вирізати зайву лінію визначення, дозволяючи використовувати var +1
BrianH

2
Зауважте також, що якщо "key"його немає, xбуде ініціалізовано доdefault(TValue)
Петро Дуніхо

Може бути приємним, як загальне розширення, теж викликається як"key".DoSomethingWithThis()
Росс Пресер

Я дуже віддаю перевагу конструкцій, які використовують getOrElse("key", defaultValue) об’єкт Null, як і раніше, є моїм улюбленим малюнком. Працюйте таким чином, і вам не байдуже, TryGetValueповертається справжнє чи хибне.
candied_orange

1
Я відчуваю, що ця відповідь заслуговує на відмову від того, що писати компактний код заради його компактності - це погано. Це може зробити ваш код набагато важчим для читання. Крім того, якщо я правильно пам'ятаю, TryGetValueце не безпечно для атомів / потоків, так що ви можете так само легко виконати одну перевірку на існування та іншу, щоб схопити та оперувати значенням
Marie,

12

Це не є ні запахом коду, ні анти-шаблоном, оскільки використання функцій стилю TryGet з параметром out є ідіоматичним C #. Однак у C # передбачено 3 варіанти роботи зі словником, тому ви повинні бути впевнені, що використовуєте правильний для своєї ситуації. Я думаю, я знаю, звідки походить чутка про те, що виникає проблема із використанням параметру out, тому я в кінці справи.

Які функції використовувати під час роботи зі словником C #:

  1. Якщо ви впевнені, що ключ буде у словнику, використовуйте властивість Item [TKey]
  2. Якщо ключ, як правило, має бути у словнику, але він поганий / рідкісний / проблематичний, що його немає, слід скористатися спробувати.
  3. Якщо ви не впевнені, що ключ буде у словнику, використовуйте TryGet з параметром out

Щоб виправдати це, потрібно лише звернутися до документації для словника TryGetValue у розділі "Зауваження" :

Цей метод поєднує функціональність методу ContainsKey та властивості Item [TKey].

...

Використовуйте метод TryGetValue, якщо ваш код часто намагається отримати доступ до ключів, яких немає у словнику. Використання цього методу є більш ефективним, ніж ловити KeyNotFoundException, кинутого властивістю Item [TKey].

Цей метод наближається до операції O (1).

Вся причина, чому існує TryGetValue, - це діяти як більш зручний спосіб використання ContainsKey та Item [TKey], уникаючи при цьому пошуку словника двічі - тому робити вигляд, що його немає, і робити дві речі, які він робить вручну, - досить незручно. вибір.

На практиці я рідко коли-небудь використовував сирий словник, завдяки цій простій максимізі: вибирайте найзагальніший клас / контейнер, який надає вам потрібну функціональність . Словник розроблений не для пошуку за значенням, а не за клавішею (наприклад), тому, якщо ви чогось цього хочете, може мати більше сенсу використовувати альтернативну структуру. Я думаю, що, можливо, я використовував Словник один раз у минулому році, який я робив, - просто тому, що він рідко був правильним інструментом для роботи, яку я намагався зробити. Словник, безумовно, не є ножем швейцарської армії із панелі інструментів C #.

Що не так із параметрами?

CA1021: Уникайте параметрів

Хоча повернені значення є звичайними і широко використовуються, правильне застосування параметрів виходу та ref вимагає проміжних навичок проектування та кодування. Бібліотечні архітектори, які розробляють дизайн для загальної аудиторії, не повинні сподіватися, що користувачі освоїтимуть роботу з параметрами або переробляти їх.

Я здогадуюсь, що саме тут ви почули, що параметри були чимось на зразок антидіаграми. Як і у всіх правилах, вам слід ознайомитися ближче, щоб зрозуміти "чому", і в цьому випадку є навіть чітка згадка про те, як шаблон "Спроба" не порушує правило :

Методи, що реалізують шаблон "Спроба", наприклад System.Int32.TryParse, не порушують це порушення.


Цілий список рекомендацій - це те, що я б хотів, щоб читали більше людей. Для тих, хто слідує, ось корінь цього. docs.microsoft.com/en-us/visualstudio/code-quality/…
Peter Wone

10

У словниках C # відсутні як мінімум два методи, які, на мою думку, значно очищують код у багатьох ситуаціях на інших мовах. Перший - це повернення Option, що дозволяє писати такий код, як наступний у Scala:

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

Другий - це повернення визначеного користувачем значення за замовчуванням, якщо ключ не знайдено:

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

Що можна сказати для використання ідіом, які мова надає, коли це доречно, як, наприклад, Tryшаблон, але це не означає, що ви повинні використовувати лише те, що надає мова. Ми програмісти. Добре створювати нові абстракції, щоб полегшити розуміння нашої конкретної ситуації, особливо якщо це виключає багато повторень. Якщо вам часто потрібно щось, наприклад, зворотний пошук або повторення значень, зробіть це. Створіть потрібний інтерфейс.


Мені подобається 2-й варіант. Можливо, мені доведеться написати метод розширення або два :)
Адам Б

2
з плюсової сторони c # розширення методів означає, що ви можете їх реалізувати самостійно
jk.

5

Це 4 рядки коду, щоб по суті зробити наступне: DoSomethingWith(dict["key"])

Я згоден, що це неелегантно. Механізм, який я люблю використовувати в цьому випадку, де значенням є тип структури, є:

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

Тепер у нас є нова версія, TryGetValueяка повертається int?. Тоді ми можемо зробити аналогічний трюк для розширення T?:

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

А тепер складіть це:

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

і ми переходимо до єдиного чіткого твердження.

Я чув, що використання ключового слова out - це антидіапазон, оскільки він змушує функції мутувати свої параметри.

Я б хотів трохи менш сильно сказати і сказати, що уникати мутацій, коли це можливо, є хорошою ідеєю.

Мені здається, що мені часто потрібен "зворотний" словник, де я гортаю клавіші та значення.

Потім реалізуйте або отримайте двонаправлений словник. Їх просто писати, або в Інтернеті є багато реалізацій. Тут є купа реалізацій, наприклад:

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

Так само я часто хотів би переглядати елементи словника і перетворювати ключі або значення в списки тощо, щоб зробити це краще.

Звичайно, ми все робимо.

Я відчуваю, що майже завжди є кращий, більш елегантний спосіб використання словників, але я в збитку.

Запитайте себе: "припустимо, у мене був клас, відмінний від того, Dictionaryщо реалізував саме ту операцію, яку я хочу зробити; як би виглядав цей клас?" Потім, як тільки ви відповіли на це запитання, реалізуйте цей клас . Ви програміст з комп’ютера. Вирішіть свої проблеми, написавши комп’ютерні програми!


Спасибі. Як ви думаєте, ви могли б трохи більше пояснити, що насправді робить метод дій? Я новачок у вчинках.
Адам Б

1
@AdamB дія - це об'єкт, який представляє можливість викликати недійсний метод. Як бачите, на стороні виклику ми передаємо ім'я недійсного методу, список параметрів якого відповідає аргументам загального типу дії. На стороні абонента дія викликається, як і будь-який інший метод.
Ерік Ліпперт

1
@AdamB: Ви можете думати Action<T>, що це дуже схоже на interface IAction<T> { void Invoke(T t); }, але з дуже м'якими правилами щодо того, що "реалізує" цей інтерфейс, і про те, як Invokeможна використовувати. Якщо ви хочете дізнатися більше, дізнайтеся про "делегатів" в C #, а потім дізнайтеся про лямбда-вирази.
Ерік Ліпперт

Гаразд я думаю, що я це зрозумів. Отже, дія дозволить вам поставити метод в якості параметра для DoIt (). І називає цей метод.
Адам Б

@AdamB: Точно так. Це приклад "функціонального програмування з функціями вищого порядку". Більшість функцій приймають дані як аргументи; Функції вищого порядку беруть функції як аргументи, а потім виконують завдання з цими функціями. Коли ви дізнаєтесь більше C #, ви побачите, що LINQ повністю реалізований з функціями вищого порядку.
Ерік Ліпперт

4

TryGetValue()Конструкція необхідна тільки якщо ви не знаєте , чи присутній в якості ключа в словнику чи ні «ключ», в іншому випадку DoSomethingWith(dict["key"])цілком допустимо.

Можливо, "менш брудним" підходом буде використовуватись ContainsKey()як чек.


6
"Можливо," менш брудним "підходом буде використання ContainsKey () як чека." Я не погоджуюсь. Настільки ж неоптимальні, як TryGetValueмінімум, важко забути поводитися з порожнім корпусом. В ідеалі, я б хотів, щоб це просто повернуло необов'язково.
Олександр - Відновіть Моніку

1
Існує потенційна проблема з ContainsKeyпідходом до багатопотокових додатків, який є часом перевірки вразливості до часу використання (TOCTOU). Що робити, якщо якийсь інший потік видаляє ключ між дзвінками до ContainsKeyта GetValue?
Девід Хаммен

2
@JAD Необов’язково складати. Ви можете матиOptional<Optional<T>>
Олександр - Відновити Моніку

2
@DavidHammen Щоб було ясно, вам потрібно буде TryGetValueна ConcurrentDictionary. Звичайний словник не був би синхронізований
Олександр - Відновити Моніку

2
@JAD Ні, (необов'язковий тип Java дме, не запускайте мене). Я кажу більш абстрактно
Олександр - Відновіть Моніку

2

Інші відповіді містять чудові моменти, тому я не буду їх перезавантажувати тут, але замість цього я зупинюсь на цій частині, яка, здається, поки що ігнорується:

Так само я часто хотів би переглядати елементи словника і перетворювати ключі або значення в списки тощо, щоб зробити це краще.

Насправді, це досить легко переробити словник, оскільки він реалізує IEnumerable:

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

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

Якщо ви віддаєте перевагу Linq, це теж виходить чудово:

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

Загалом, я не вважаю, що Словники є антипатерном - вони просто специфічний інструмент із специфічним використанням.

Також для отримання більш детальної інформації ознайомтеся SortedDictionary(використовує дерева RB для більш передбачуваної продуктивності) та SortedList(що також є заплутано названим словником, який жертвує швидкістю вставки для швидкості пошуку, але світить, якщо ви дивитесь на фіксовану, попередньо відсортований набір). У мене був випадок, коли заміна аDictionary з SortedDictionaryпривела в порядок швидше виконання (але це може статися навпаки теж).


1

Якщо ви вважаєте, що використання словника незручно, це може бути не правильним вибором для вашої проблеми. Словники чудові, але, як зауважив один коментатор, часто вони використовуються як ярлик для чогось, що мало бути класом. Або сам словник може бути правильним як основний спосіб зберігання, але навколо нього повинен бути клас обгортки, щоб забезпечити бажані методи обслуговування.

Про Dict [key] проти TryGet було сказано багато. Що я багато використовую, це повторення словника за допомогою KeyValuePair. Мабуть, це менш відома конструкція.

Основна користь словника полягає в тому, що він дійсно швидкий порівняно з іншими колекціями, оскільки кількість предметів зростає. Якщо вам доведеться перевірити чи багато клавіш, ви можете запитати себе, чи правильно використовувати словник. Як клієнт, ти, як правило, повинен знати, що ти туди вкладаєш, і таким чином, що було б безпечно запитувати.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.