Existe uma implementação de IDictionary que, na chave ausente, retorna o valor padrão em vez de lançar?

129

O indexador no Dictionary lança uma exceção se a chave estiver ausente. Existe uma implementação do IDictionary que retornará o padrão (T)?

Eu sei sobre o método "TryGetValue", mas isso é impossível de usar com o linq.

Isso faria com eficiência o que eu preciso ?:

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

Eu não acho que vai, pois acho que irá iterar as chaves em vez de usar uma pesquisa Hash.

TheSoftwareJedi
fonte
10
Veja também esta questão mais tarde, marcado como uma duplicata de um presente, mas com diferentes respostas: Dicionário de retornar um valor padrão se a chave não existe
Rory O'Kane
2
@ dylan Isso lançará uma exceção na chave ausente, não nula. Além disso, seria necessário decidir qual deveria ser o padrão em qualquer lugar em que o dicionário seja usado. Observe também a idade desta pergunta. Nós não tivemos o ?? operador há 8 anos de qualquer maneira
TheSoftwareJedi

Respostas:

147

Na verdade, isso não será nada eficiente.

Você sempre pode escrever um método de extensão:

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;
}

Ou com C # 7.1:

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

Que usa:

  • Um método de expressão corporal (C # 6)
  • Uma variável de saída (C # 7.0)
  • Um literal padrão (C # 7.1)
Jon Skeet
fonte
2
Ou de forma mais compacta:return (dictionary.ContainsKey(key)) ? dictionary[key] : default(TValue);
Peter Gluck
33
@ PeterGluck: De forma mais compacta, mas com menos eficiência ... por que realizar duas pesquisas no caso em que a chave está presente?
9138 Jon Skeet
5
@ JonSkeet: Obrigado por corrigir Peter; Eu tenho usado esse método "menos eficiente" sem perceber por um tempo agora.
JJ Bryan Price
5
Muito agradável! Vou plagiar descaradamente - er, garfo. ;)
Jon Coombs
3
Aparentemente, a MS decidiu que isso era bom o suficiente para adicionar System.Collections.Generic.CollectionExtensions, como eu apenas tentei e está lá.
theMayer 21/02/19
19

Carregar esses métodos de extensão pode ajudar ..

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();
}
nawfal
fonte
3
A última sobrecarga é interessante. Uma vez que não há nada para selecionar a partir de , esta é simplesmente uma forma de avaliação preguiçosa?
MEMark
1
O seletor de valor @MEMark yes é executado apenas se necessário, de modo que pode ser considerado uma avaliação lenta, mas não é uma execução adiada, como no contexto do Linq. Mas a avaliação preguiçosa aqui não depende de nada para selecionar o fator, é claro que você pode fazer o seletor Func<K, V>.
Nawfal 20/03
1
Esse terceiro método é público porque é útil por si só? Ou seja, você está pensando que alguém pode passar um lambda que usa a hora atual ou um cálculo baseado em ... hmm. Eu esperava apenas ter o segundo método do valor V; return dict.TryGetValue (chave, valor de saída)? valor: defVal;
Jon Coombs
1
@JCoombs certo, a terceira sobrecarga existe para o mesmo objetivo. E é claro que você pode fazer isso em uma segunda sobrecarga, mas eu estava ficando um pouco seco (para não repetir a lógica).
Nawfal
4
@nawfal Bom ponto. Permanecer seco é mais importante do que evitar uma chamada de método escassa.
Jon Coombs
19

Se alguém usando o .net core 2 e superior (C # 7.X), a classe CollectionExtensions é introduzida e pode usar o método GetValueOrDefault para obter o valor padrão se a chave não estiver presente em um dicionário.

Dictionary<string, string> colorData = new  Dictionary<string, string>();
string color = colorData.GetValueOrDefault("colorId", string.Empty);
cdev
fonte
4

Collections.Specialized.StringDictionaryfornece um resultado de não exceção ao procurar o valor de uma chave ausente. Também não diferencia maiúsculas de minúsculas por padrão.

Ressalvas

Ele é válido apenas para usos especializados e, sendo projetado antes dos genéricos, não possui um enumerador muito bom se você precisar revisar toda a coleção.

Mark Hurd
fonte
4

Se você estiver usando o .Net Core, poderá usar o método CollectionExtensions.GetValueOrDefault . É o mesmo que a implementação fornecida na resposta aceita.

public static TValue GetValueOrDefault<TKey,TValue> (
   this System.Collections.Generic.IReadOnlyDictionary<TKey,TValue> dictionary,
   TKey key);
theMayer
fonte
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();
    }
}
Brian
fonte
1

Pode-se definir uma interface para a função de pesquisa de teclas de um dicionário. Eu provavelmente defini-lo como algo como:

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

A versão com chaves não genéricas permitiria que o código estivesse usando código usando tipos de chave não estruturados para permitir variação arbitrária de chave, o que não seria possível com um parâmetro de tipo genérico. Não se deve permitir o uso de um mutável Dictionary(Of Cat, String)como mutável, Dictionary(Of Animal, String)pois o último permitiria SomeDictionaryOfCat.Add(FionaTheFish, "Fiona"). Mas não há nada de errado em usar um mutável Dictionary(Of Cat, String)como imutável Dictionary(Of Animal, String), pois SomeDictionaryOfCat.Contains(FionaTheFish)deve ser considerada uma expressão perfeitamente bem formada (deve retornarfalse , sem ter que pesquisar no dicionário, por algo que não seja do tipo Cat).

Infelizmente, a única maneira de conseguir usar essa interface é se um Dictionaryobjeto é agrupado em uma classe que implementa a interface. Dependendo do que você está fazendo, no entanto, essa interface e a variação que ela permite podem valer a pena.

supercat
fonte
1

Se você estiver usando o ASP.NET MVC, poderá aproveitar a classe RouteValueDictionary que executa o trabalho.

public object this[string key]
{
  get
  {
    object obj;
    this.TryGetValue(key, out obj);
    return obj;
  }
  set
  {
    this._dictionary[key] = value;
  }
}
Jone Polvora
fonte
1

Essa pergunta ajudou a confirmar que as TryGetValuepeças jogadasFirstOrDefault desempenha aqui.

Um recurso interessante do C # 7 que eu gostaria de mencionar é o recurso de variáveis ​​out , e se você adicionar o operador condicional nulo do C # 6 à equação, seu código poderá ser muito mais simples, sem a necessidade de métodos de extensão extra.

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

A desvantagem disso é que você não pode fazer tudo em linha assim;

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

Se precisarmos / queremos fazer isso, devemos codificar um método de extensão como o de Jon.

hardkoded
fonte
1

Aqui está uma versão do @ JonSkeet para o mundo do C # 7.1 que também permite que um padrão opcional seja passado:

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

Pode ser mais eficiente ter duas funções para otimizar o caso em que você deseja retornar 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;
}

Infelizmente, o C # (ainda?) Não possui um operador de vírgula (ou o operador de ponto-e-vírgula proposto pelo C # 6); portanto, você precisa ter um corpo de função real (suspiro!) Para uma das sobrecargas.

NetMage
fonte
1

Usei o encapsulamento para criar um IDictionary com comportamento muito semelhante a um mapa STL , para aqueles que estão familiarizados com c ++. Para quem não é:

  • O indexador get {} no SafeDictionary abaixo retorna o valor padrão se uma chave não estiver presente e adiciona essa chave ao dicionário com um valor padrão. Esse é geralmente o comportamento desejado, pois você procura itens que aparecerão eventualmente ou que têm uma boa chance de aparecer.
  • O método Add (chave TK, TV val) se comporta como um método AddOrUpdate, substituindo o valor presente, se existir, em vez de jogar. Não vejo por que m $ não possui um método AddOrUpdate e acha que lançar erros em cenários muito comuns é uma boa ideia.

TL / DR - O SafeDictionary é gravado de forma a nunca lançar exceções sob nenhuma circunstância, exceto em cenários perversos , como o computador ficar sem memória (ou pegando fogo). Isso é feito substituindo o comportamento Add with AddOrUpdate e retornando o padrão em vez de gerar NotFoundException no indexador.

Aqui está o código:

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();
    }
}
neural
fonte
1

Desde o .NET core 2.0, você pode usar:

myDict.GetValueOrDefault(someKeyKalue)
Antoine L
fonte
0

O que acontece com essa linha única que verifica se uma chave está presente usando ContainsKeye depois retorna o valor normalmente recuperado ou o valor padrão usando o operador condicional?

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

Não há necessidade de implementar uma nova classe Dictionary que suporte valores padrão, basta substituir suas instruções de pesquisa pela linha curta acima.

Byte Commander
fonte
5
Isso tem duas pesquisas em vez de uma e introduz uma condição de corrida se você estiver usando o ConcurrentDictionary.
Piedar # 7/16
0

Pode ser uma linha única para verificar TryGetValuee retornar o valor padrão, se for o caso 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

Experimente online

Aryan Firouzian
fonte
-1

Não, porque, caso contrário, como você saberia a diferença quando a chave existir, mas armazenará um valor nulo? Isso pode ser significativo.

Joel Coehoorn
fonte
11
Poderia ser - mas em algumas situações você pode muito bem saber que não é. Eu posso ver isso sendo útil às vezes.
11119 Jon Skeet
8
Se você sabe que o null não está lá - é muito bom ter isso. Ele faz juntar estas coisas em Linq (que é tudo o que faço ultimamente) muito mais fácil
TheSoftwareJedi
1
Usando o método ContainsKey?
precisa saber é o seguinte
4
Sim, é realmente muito útil. O Python tem isso e eu o uso muito mais do que o método simples, quando por acaso estou no Python. Salva a gravação de muitas instruções if extras que estão apenas verificando resultados nulos. (Você está certo, é claro, que nula é ocasionalmente útil, mas geralmente eu estou querendo um "" ou 0 em vez.)
Jon Coombs
Eu tinha votado nisso. Mas se fosse hoje eu não teria. Não posso cancelar agora :). I upvoted os comentários para fazer o meu ponto :)
Nawfal