Есть ли лучший способ использовать словари C #, чем TryGetValue?

19

Я часто просматриваю вопросы в Интернете, и многие решения включают словари. Однако всякий раз, когда я пытаюсь реализовать их, я получаю этот ужасный запах в своем коде. Например, каждый раз, когда я хочу использовать значение:

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

Это 4 строки кода, чтобы по существу сделать следующее: DoSomethingWith(dict["key"])

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

Кроме того, я часто нуждаюсь в «обратном» словаре, где я переворачиваю ключи и значения.

Точно так же я часто хотел бы перебирать элементы в словаре и находить себя конвертирующим ключи или значения в списки и т.д., чтобы сделать это лучше.

Я чувствую, что почти всегда есть лучший, более элегантный способ использования словарей, но я в растерянности.

Адам Б
источник
7
Могут существовать и другие способы, но я обычно использую ContainsKey, прежде чем пытаться получить значение.
Робби Ди
2
Честно говоря, за некоторыми исключениями, если вы знаете, что ваши ключи словаря опережают время, вероятно, есть лучший способ. Если вы не знаете, в общем случае, клавиши dict вообще не должны быть жестко закодированы. Словари полезны для работы с полуструктурированными объектами или данными, где поля, по крайней мере, в большинстве своем ортогональны приложению. ИМО: чем ближе эти области становятся релевантными концепциями бизнеса / предметной области, тем менее полезными становятся словари для работы с этими концепциями.
Свидген
6
@RobbieDee: вы должны быть осторожны, когда делаете это, так как это создает условия гонки. Возможно, ключ может быть удален между вызовом ContainsKey и получением значения.
whatsisname
12
@whatsisname: в этих условиях лучше использовать ConcurrentDictionary . Коллекции в System.Collections.Genericпространстве имен не являются потокобезопасными .
Роберт Харви
4
Хорошие 50% времени я вижу, что кто-то использует Dictionary, что они действительно хотели, это новый класс. Для этих 50% (только) это дизайнерский запах.
BlueRaja - Дэнни Пфлюгофт

Ответы:

23

Словари (C # или другие) - это просто контейнер, в котором вы ищите значение на основе ключа. Во многих языках это более правильно идентифицируется как Map с наиболее распространенной реализацией, являющейся 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 # придумали идиому, чтобы разобраться с поведением». - Я думаю, что вы ударили гвоздь по голове: они «придумали» идиому, вместо того, чтобы использовать существующий широко используемый, который является необязательным типом возврата.
Йорг Миттаг
3
@ JörgWMittag Если вы говорите о чем-то подобном Option<T>, то я думаю, что это усложнит использование метода в C # 2.0, потому что в нем нет сопоставления с образцом.
свик
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()
Ross Presser
Я очень предпочитаю дизайн, который использует getOrElse("key", defaultValue) объект Null - все еще мой любимый образец. Работайте таким образом, и вам все равно, TryGetValueвернет ли значение true или false.
candied_orange
1
Я чувствую, что этот ответ заслуживает отказа от ответственности, что написание компактного кода ради его компактности плохо. Это может сделать ваш код намного сложнее для чтения. Кроме того, если я правильно помню, TryGetValueэто не атомарно / потокобезопасно, так что вы могли бы так же легко выполнить одну проверку на существование и другую, чтобы захватить и воздействовать на значение
Marie
12

Это не является ни запахом кода, ни анти-паттерном, так как использование функций в стиле TryGet с параметром out является идиоматическим C #. Тем не менее, в C # предусмотрено 3 варианта работы со словарем, поэтому вы должны быть уверены, что используете правильный вариант для вашей ситуации. Я думаю, что знаю, откуда появились слухи о наличии проблемы с использованием параметра out, поэтому я рассмотрю это в конце.

Какие функции использовать при работе со словарем C #:

  1. Если вы уверены, что ключ будет в словаре, используйте свойство Item [TKey]
  2. Если ключ обычно находится в словаре, но его плохо / редко / проблематично, что его там нет, следует использовать Try ... Catch, чтобы возникла ошибка, а затем вы можете попытаться обработать ошибку изящно
  3. Если вы не уверены, что ключ будет в словаре, используйте TryGet с параметром out

Чтобы оправдать это, достаточно обратиться к документации для словаря TryGetValue в разделе «Замечания» :

Этот метод объединяет функциональность метода ContainsKey и свойства Item [TKey].

...

Используйте метод TryGetValue, если ваш код часто пытается получить доступ к ключам, которых нет в словаре. Использование этого метода более эффективно, чем перехват исключения KeyNotFoundException, вызванного свойством Item [TKey].

Этот метод приближается к операции O (1).

Единственная причина, по которой существует TryGetValue, заключается в том, чтобы действовать как более удобный способ использования ContainsKey и Item [TKey], избегая при этом необходимости дважды искать в словаре - поэтому притворяться, что он не существует, и делать две вещи, которые он делает вручную, довольно неудобно. выбор.

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

Что не так с нашими параметрами?

CA1021: избегать параметров

Хотя возвращаемые значения являются обычным явлением и широко используются, правильное применение параметров out и ref требует промежуточных навыков проектирования и кодирования. Архитекторы библиотек, которые проектируют для широкой аудитории, не должны ожидать, что пользователи освоят работу с параметрами out или ref.

Я предполагаю, что именно здесь вы слышали, что параметры были чем-то вроде анти-паттерна. Как и со всеми правилами, вам следует прочитать поближе, чтобы понять «почему», и в этом случае даже есть явное упоминание о том, как шаблон Try не нарушает правило :

Методы, реализующие шаблон Try, такие как System.Int32.TryParse, не вызывают это нарушение.

BrianH
источник
Я хотел бы, чтобы весь этот список руководств читал больше людей. Для тех, кто следует, вот корень этого. docs.microsoft.com/en-us/visualstudio/code-quality/…
Питер Воне
10

В словарях C # отсутствуют по крайней мере два метода, которые, по моему мнению, значительно очищают код во многих ситуациях на других языках. Первый возвращает an 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 действие - это объект, который представляет возможность вызова пустого метода. Как видите, на стороне вызывающей стороны мы передаем имя метода void, список параметров которого соответствует аргументам универсального типа действия. На стороне вызывающей стороны действие вызывается, как и любой другой метод.
Эрик Липперт
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привела в порядок быстрее исполнения (но это может случиться наоборот тоже).

Vilx-
источник
1

Если вы чувствуете, что использование словаря неудобно, это может быть неправильный выбор для вашей проблемы. Словари хороши, но, как заметил один комментатор, часто они используются как ярлык для того, что должно было быть классом. Или сам словарь может быть правильным в качестве основного метода хранения, но вокруг него должен быть класс-обертка, чтобы обеспечить требуемые методы обслуживания.

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

Основным преимуществом словаря является то, что он действительно быстр по сравнению с другими коллекциями, так как количество предметов растет. Если вам нужно проверить, существует ли ключ много, вы можете спросить себя, подходит ли вам использование словаря. Как клиент, вы, как правило, должны знать, что вы положили туда и, следовательно, что было бы безопасно запрашивать.

Мартин Маат
источник