Есть ли реализация IDictionary, которая при отсутствии ключа возвращает значение по умолчанию вместо того, чтобы бросать?

129

Индексатор в Dictionary выдает исключение, если ключ отсутствует. Есть ли реализация IDictionary, которая вместо этого вернет значение по умолчанию (T)?

Я знаю о методе "TryGetValue", но его невозможно использовать с linq.

Будет ли это эффективно делать то, что мне нужно ?:

myDict.FirstOrDefault(a => a.Key == someKeyKalue);

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

TheSoftwareJedi
источник
10
См. Также этот более поздний вопрос, помеченный как дубликат этого, но с разными ответами: Словарь, возвращающий значение по умолчанию, если ключ не существует
Рори О'Кейн
2
@dylan Это вызовет исключение при отсутствии ключа, а не на null. Кроме того, потребуется решить, какое значение по умолчанию должно быть везде, где используется словарь. Также обратите внимание на возраст этого вопроса. У нас не было ?? operator 8 лет назад так или иначе
TheSoftwareJedi

Ответы:

147

В самом деле, это вообще не будет эффективно.

Вы всегда можете написать метод расширения:

public static TValue GetValueOrDefault<TKey,TValue>
    (this IDictionary<TKey, TValue> dictionary, TKey key)
{
    TValue ret;
    // Ignore return value
    dictionary.TryGetValue(key, out ret);
    return ret;
}

Или с C # 7.1:

public static TValue GetValueOrDefault<TKey,TValue>
    (this IDictionary<TKey, TValue> dictionary, TKey key) =>
    dictionary.TryGetValue(key, out var ret) ? ret : default;

Это использует:

  • Метод, воплощающий выражение (C # 6)
  • Выходная переменная (C # 7.0)
  • Литерал по умолчанию (C # 7.1)
Джон Скит
источник
2
Или более компактно:return (dictionary.ContainsKey(key)) ? dictionary[key] : default(TValue);
Питер Глюк
33
@PeterGluck: Компактнее, но менее эффективно ... зачем выполнять два поиска в случае, если ключ присутствует?
Джон Скит,
5
@JonSkeet: Спасибо, что поправили Питера; Я уже давно использую этот «менее эффективный» метод, даже не осознавая этого.
J Bryan Price
5
Очень хорошо! Я собираюсь беззастенчиво заниматься плагиатом - ну, развелись. ;)
Джон Кумбс
3
По-видимому, MS решила, что этого достаточно, чтобы добавить к нему System.Collections.Generic.CollectionExtensions, поскольку я только что попробовал, и он есть.
theMayer
19

Использование этих методов расширения может помочь ..

public static V GetValueOrDefault<K, V>(this IDictionary<K, V> dict, K key)
{
    return dict.GetValueOrDefault(key, default(V));
}

public static V GetValueOrDefault<K, V>(this IDictionary<K, V> dict, K key, V defVal)
{
    return dict.GetValueOrDefault(key, () => defVal);
}

public static V GetValueOrDefault<K, V>(this IDictionary<K, V> dict, K key, Func<V> defValSelector)
{
    V value;
    return dict.TryGetValue(key, out value) ? value : defValSelector();
}
Навфал
источник
3
Интересна последняя перегрузка. Поскольку выбирать не из чего , неужели это просто форма ленивого вычисления?
MEMark
1
Селектор значений @MEMark yes запускается только при необходимости, поэтому в некотором смысле это можно назвать ленивой оценкой, но не отложенным выполнением, как в контексте Linq. Но ленивая оценка здесь не зависит от того, из чего выбирать , вы, конечно, можете сделать селектор Func<K, V>.
nawfal
1
Является ли этот третий метод общедоступным, потому что он полезен сам по себе? То есть вы думаете, что кто-то может передать лямбду, которая использует текущее время, или расчет, основанный на ... хм. Я ожидал, что второй метод будет иметь значение V; return dict.TryGetValue (ключ, выходное значение)? значение: defVal;
Джон Кумбс,
1
@JCoombs, третья перегрузка предназначена для той же цели. И, конечно, вы можете сделать это во второй перегрузке, но я делал это немного сухим (чтобы не повторять логику).
nawfal
4
@nawfal Хорошее замечание. Оставаться сухим важнее, чем избегать одного жалкого вызова метода.
Джон Кумбс,
19

Если кто-то использует .net core 2 и выше (C # 7.X), вводится класс CollectionExtensions, который может использовать метод GetValueOrDefault для получения значения по умолчанию, если ключа нет в словаре.

Dictionary<string, string> colorData = new  Dictionary<string, string>();
string color = colorData.GetValueOrDefault("colorId", string.Empty);
cdev
источник
4

Collections.Specialized.StringDictionaryобеспечивает неисключительный результат при поиске значения отсутствующего ключа. По умолчанию регистр не учитывается.

Предостережения

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

Марк Херд
источник
4

Если вы используете .Net Core, вы можете использовать метод CollectionExtensions.GetValueOrDefault . Это то же самое, что и реализация, представленная в принятом ответе.

public static TValue GetValueOrDefault<TKey,TValue> (
   this System.Collections.Generic.IReadOnlyDictionary<TKey,TValue> dictionary,
   TKey key);
theMayer
источник
3
public class DefaultIndexerDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
    private IDictionary<TKey, TValue> _dict = new Dictionary<TKey, TValue>();

    public TValue this[TKey key]
    {
        get
        {
            TValue val;
            if (!TryGetValue(key, out val))
                return default(TValue);
            return val;
        }

        set { _dict[key] = value; }
    }

    public ICollection<TKey> Keys => _dict.Keys;

    public ICollection<TValue> Values => _dict.Values;

    public int Count => _dict.Count;

    public bool IsReadOnly => _dict.IsReadOnly;

    public void Add(TKey key, TValue value)
    {
        _dict.Add(key, value);
    }

    public void Add(KeyValuePair<TKey, TValue> item)
    {
        _dict.Add(item);
    }

    public void Clear()
    {
        _dict.Clear();
    }

    public bool Contains(KeyValuePair<TKey, TValue> item)
    {
        return _dict.Contains(item);
    }

    public bool ContainsKey(TKey key)
    {
        return _dict.ContainsKey(key);
    }

    public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
    {
        _dict.CopyTo(array, arrayIndex);
    }

    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
    {
        return _dict.GetEnumerator();
    }

    public bool Remove(TKey key)
    {
        return _dict.Remove(key);
    }

    public bool Remove(KeyValuePair<TKey, TValue> item)
    {
        return _dict.Remove(item);
    }

    public bool TryGetValue(TKey key, out TValue value)
    {
        return _dict.TryGetValue(key, out value);
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return _dict.GetEnumerator();
    }
}
Брайан
источник
1

Можно определить интерфейс для функции поиска по ключу словаря. Я бы, наверное, определил это примерно так:

Interface IKeyLookup(Of Out TValue)
  Function Contains(Key As Object)
  Function GetValueIfExists(Key As Object) As TValue
  Function GetValueIfExists(Key As Object, ByRef Succeeded As Boolean) As TValue
End Interface

Interface IKeyLookup(Of In TKey, Out TValue)
  Inherits IKeyLookup(Of Out TValue)
  Function Contains(Key As TKey)
  Function GetValue(Key As TKey) As TValue
  Function GetValueIfExists(Key As TKey) As TValue
  Function GetValueIfExists(Key As TKey, ByRef Succeeded As Boolean) As TValue
End Interface

Версия с неуниверсальными ключами позволит коду, который использует код, использующий неструктурированные типы ключей, допускать произвольную вариацию ключа, что невозможно с параметром универсального типа. Нельзя позволять использовать изменяемый Dictionary(Of Cat, String)объект как изменяемый, Dictionary(Of Animal, String)поскольку последний позволитSomeDictionaryOfCat.Add(FionaTheFish, "Fiona") . Но нет ничего плохого в использовании изменяемого Dictionary(Of Cat, String)в качестве неизменяемого Dictionary(Of Animal, String), поскольку его SomeDictionaryOfCat.Contains(FionaTheFish)следует рассматривать как совершенно правильно сформированное выражение (оно должно возвращать falseбез необходимости поиска в словаре все, что не относится к типу Cat).

К сожалению, единственный способ действительно использовать такой интерфейс - это обернуть Dictionary объект в класс, реализующий интерфейс. Однако в зависимости от того, что вы делаете, такой интерфейс и допускаемая им вариативность могут оправдать ваши усилия.

Supercat
источник
1

Если вы используете ASP.NET MVC, вы можете использовать класс RouteValueDictionary, который выполняет эту работу.

public object this[string key]
{
  get
  {
    object obj;
    this.TryGetValue(key, out obj);
    return obj;
  }
  set
  {
    this._dictionary[key] = value;
  }
}
Йоне Полвора
источник
1

Этот вопрос помог подтвердить , что TryGetValueиграетFirstOrDefault роль.

Одна интересная функция C # 7, о которой я хотел бы упомянуть, - это функция выходных переменных , и если вы добавите в уравнение условный оператор null из C # 6, ваш код может быть намного проще без необходимости в дополнительных методах расширения.

var dic = new Dictionary<string, MyClass>();
dic.TryGetValue("Test", out var item);
item?.DoSomething();

Обратной стороной этого является то, что вы не можете делать все так встроенно;

dic.TryGetValue("Test", out var item)?.DoSomething();

Если нам нужно / мы хотим это сделать, мы должны закодировать один метод расширения, такой как у Джона.

hardkoded
источник
1

Вот версия @ JonSkeet для мира C # 7.1, которая также позволяет передавать необязательное значение по умолчанию:

public static TV GetValueOrDefault<TK, TV>(this IDictionary<TK, TV> dict, TK key, TV defaultValue = default) => dict.TryGetValue(key, out TV value) ? value : defaultValue;

Возможно, будет более эффективным иметь две функции для оптимизации случая, когда вы хотите вернуть default(TV):

public static TV GetValueOrDefault<TK, TV>(this IDictionary<TK, TV> dict, TK key, TV defaultValue) => dict.TryGetValue(key, out TV value) ? value : defaultValue;
public static TV GetValueOrDefault2<TK, TV>(this IDictionary<TK, TV> dict, TK key) {
    dict.TryGetValue(key, out TV value);
    return value;
}

К сожалению, в C # (пока?) Нет оператора запятой (или оператора точки с запятой, предложенного в C # 6), поэтому вам нужно иметь фактическое тело функции (ах!) Для одной из перегрузок.

NetMage
источник
1

Я использовал инкапсуляцию для создания IDictionary с поведением, очень похожим на карту STL , для тех из вас, кто знаком с C ++. Для тех, у кого нет:

  • indexer get {} в SafeDictionary ниже возвращает значение по умолчанию, если ключ отсутствует, и добавляет этот ключ в словарь со значением по умолчанию. Часто это желаемое поведение, поскольку вы ищете элементы, которые со временем появятся или имеют хорошие шансы появиться.
  • метод Add (ключ TK, TV val) ведет себя как метод AddOrUpdate, заменяя существующее значение, если оно существует, вместо того, чтобы бросать. Я не понимаю, почему у m $ нет метода AddOrUpdate, и считает, что выдача ошибок в очень распространенных сценариях - хорошая идея.

TL / DR - SafeDictionary написан так, чтобы никогда не вызывать исключения ни при каких обстоятельствах, кроме извращенных сценариев. , таких как памяти (или пожар) компьютера. Для этого он заменяет Add на поведение AddOrUpdate и возвращает значение по умолчанию вместо выдачи NotFoundException из индексатора.

Вот код:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

public class SafeDictionary<TK, TD>: IDictionary<TK, TD> {
    Dictionary<TK, TD> _underlying = new Dictionary<TK, TD>();
    public ICollection<TK> Keys => _underlying.Keys;
    public ICollection<TD> Values => _underlying.Values;
    public int Count => _underlying.Count;
    public bool IsReadOnly => false;

    public TD this[TK index] {
        get {
            TD data;
            if (_underlying.TryGetValue(index, out data)) {
                return data;
            }
            _underlying[index] = default(TD);
            return default(TD);
        }
        set {
            _underlying[index] = value;
        }
    }

    public void CopyTo(KeyValuePair<TK, TD>[] array, int arrayIndex) {
        Array.Copy(_underlying.ToArray(), 0, array, arrayIndex,
            Math.Min(array.Length - arrayIndex, _underlying.Count));
    }


    public void Add(TK key, TD value) {
        _underlying[key] = value;
    }

    public void Add(KeyValuePair<TK, TD> item) {
        _underlying[item.Key] = item.Value;
    }

    public void Clear() {
        _underlying.Clear();
    }

    public bool Contains(KeyValuePair<TK, TD> item) {
        return _underlying.Contains(item);
    }

    public bool ContainsKey(TK key) {
        return _underlying.ContainsKey(key);
    }

    public IEnumerator<KeyValuePair<TK, TD>> GetEnumerator() {
        return _underlying.GetEnumerator();
    }

    public bool Remove(TK key) {
        return _underlying.Remove(key);
    }

    public bool Remove(KeyValuePair<TK, TD> item) {
        return _underlying.Remove(item.Key);
    }

    public bool TryGetValue(TK key, out TD value) {
        return _underlying.TryGetValue(key, out value);
    }

    IEnumerator IEnumerable.GetEnumerator() {
        return _underlying.GetEnumerator();
    }
}
нервный
источник
1

Начиная с .NET core 2.0 вы можете использовать:

myDict.GetValueOrDefault(someKeyKalue)
Антуан Л
источник
0

Как насчет этого однострочника, который проверяет наличие ключа с помощью, ContainsKeyа затем возвращает либо обычно полученное значение, либо значение по умолчанию с использованием условного оператора?

var myValue = myDictionary.ContainsKey(myKey) ? myDictionary[myKey] : myDefaultValue;

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

Байт-командир
источник
5
У этого есть два поиска вместо одного, и это вводит условие гонки, если вы используете ConcurrentDictionary.
piedar 07
0

Это может быть однострочный запрос для проверки TryGetValueи возврата значения по умолчанию, если это так false.

 Dictionary<string, int> myDic = new Dictionary<string, int>() { { "One", 1 }, { "Four", 4} };
 string myKey = "One"
 int value = myDic.TryGetValue(myKey, out value) ? value : 100;

myKey = "One" => value = 1

myKey = "two" => value = 100

myKey = "Four" => value = 4

Попробуйте онлайн

Ариан Фирузян
источник
-1

Нет, потому что в противном случае, как бы вы узнали разницу, когда ключ существует, но хранит нулевое значение? Это могло быть значительным.

Джоэл Кохорн
источник
11
Может быть, но в некоторых ситуациях вы можете знать, что это не так. Иногда я вижу, что это полезно.
Джон Скит,
8
Если вы знаете, что null там нет - это очень приятно иметь. Это значительно упрощает присоединение к этим вещам в Linq (а это все, чем я занимаюсь в последнее время)
TheSoftwareJedi
1
Используя метод ContainsKey?
MattDavey
4
Да, это действительно очень полезно. У Python это есть, и я использую его гораздо чаще, чем простой метод, когда я работаю на Python. Экономит написание большого количества дополнительных операторов if, которые просто проверяют нулевой результат. (Вы правы, конечно, этот null иногда бывает полезен, но обычно я хочу вместо него "" или 0.)
Джон Кумбс
Я проголосовал за это. Но если бы это было сегодня, я бы не стал. Не могу отменить сейчас :). Я проголосовал за комментарии, чтобы выразить свою точку зрения :)
nawfal