Existe uma maneira melhor de usar dicionários C # do que TryGetValue?

19

Muitas vezes, encontro perguntas online, e muitas soluções incluem dicionários. No entanto, sempre que tento implementá-los, recebo esse cheiro horrível no meu código. Por exemplo, toda vez que eu quero usar um valor:

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

São 4 linhas de código para essencialmente fazer o seguinte: DoSomethingWith(dict["key"])

Ouvi dizer que o uso da palavra-chave out é um anti-padrão porque faz com que as funções alterem seus parâmetros.

Além disso, muitas vezes me vejo precisando de um dicionário "invertido", onde inverto as chaves e os valores.

Da mesma forma, muitas vezes eu gostaria de percorrer os itens de um dicionário e me encontrar convertendo chaves ou valores em listas etc. para fazer isso melhor.

Sinto que quase sempre há uma maneira melhor e mais elegante de usar dicionários, mas estou perdida.

Adam B
fonte
7
Podem existir outras formas, mas geralmente uso primeiro um ContainsKey antes de tentar obter um valor.
Robbie Dee
2
Honestamente, com algumas exceções, se você sabe quais são as chaves do seu dicionário antes, provavelmente existe uma maneira melhor. Se você não sabe, as chaves do ditado geralmente não devem ser codificadas. Os dicionários são úteis para trabalhar com objetos ou dados semiestruturados nos quais os campos são pelo menos principalmente ortogonais ao aplicativo. Na IMO, quanto mais próximos esses campos estiverem dos conceitos relevantes de negócios / domínio, menos dicionários úteis serão para trabalhar com esses conceitos.
svidgen 27/08
6
@RobbieDee: você precisa ter cuidado ao fazer isso, pois isso cria uma condição de corrida. É possível que a chave possa ser removida entre chamar ContainsKey e obter o valor.
whatsisname
12
@ whatsisname: Nessas condições, um ConcurrentDictionary seria mais adequado. Coleções no System.Collections.Genericnamespace não são seguras para threads .
Robert Harvey
4
Em 50% das vezes, vejo alguém usando um Dictionary, o que eles realmente queriam era uma nova classe. Para aqueles 50% (apenas), é um cheiro de design.
BlueRaja - Danny Pflughoeft

Respostas:

23

Dicionários (C # ou outros) são simplesmente um contêiner onde você procura um valor com base em uma chave. Em muitos idiomas, é mais corretamente identificado como um mapa, com a implementação mais comum sendo um HashMap.

O problema a considerar é o que acontece quando uma chave não existe. Alguns idiomas se comportam retornando nullou nilou algum outro valor equivalente. Predefinir silenciosamente um valor em vez de informar que um valor não existe.

Para o bem ou para o mal, os designers da biblioteca C # criaram um idioma para lidar com o comportamento. Eles argumentaram que o comportamento padrão para procurar um valor que não existe é lançar uma exceção. Se você deseja evitar exceções, pode usar a Tryvariante. É a mesma abordagem usada para analisar seqüências de caracteres em números inteiros ou objetos de data / hora. Essencialmente, o impacto é assim:

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

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

E isso foi transferido para o dicionário, cujo indexador delega para Get(index):

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

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

É assim que o idioma é projetado.


As outvariáveis devem ser desencorajadas?

C # não é o primeiro idioma para tê-los e eles têm seu objetivo em situações específicas. Se você estiver tentando criar um sistema altamente simultâneo, não poderá usar outvariáveis ​​nos limites de simultaneidade.

De várias maneiras, se houver um idioma adotado pelos fornecedores de idiomas e bibliotecas principais, tento adotar esses idiomas nas minhas APIs. Isso faz com que a API se sinta mais consistente e em casa nesse idioma. Portanto, um método escrito em Ruby não será parecido com um método escrito em C #, C ou Python. Cada um deles tem uma maneira preferida de criar código, e trabalhar com isso ajuda os usuários da sua API a aprendê-lo mais rapidamente.


Os mapas em geral são um antipadrão?

Eles têm seu objetivo, mas muitas vezes podem ser a solução errada para o objetivo que você tem. Particularmente se você tiver um mapeamento bidirecional, precisará. Existem muitos contêineres e maneiras de organizar dados. Existem muitas abordagens que você pode usar e, às vezes, precisa pensar um pouco antes de escolher esse contêiner.

Se você tiver uma lista muito curta de valores de mapeamento bidirecional, poderá precisar apenas de uma lista de tuplas. Ou uma lista de estruturas, onde você pode encontrar facilmente a primeira correspondência nos dois lados do mapeamento.

Pense no domínio do problema e escolha a ferramenta mais apropriada para o trabalho. Se não houver, crie-o.

Berin Loritsch
fonte
4
"então você pode precisar apenas de uma lista de tuplas. Ou de uma lista de estruturas, onde você pode facilmente encontrar a primeira correspondência nos dois lados do mapeamento." - Se houver otimizações para conjuntos pequenos, eles devem ser feitos na biblioteca, não com carimbo de borracha no código do usuário.
Blrfl
Se você escolher uma lista de tuplas ou um dicionário, isso é um detalhe da implementação. É uma questão de entender o domínio do problema e usar a ferramenta certa para o trabalho. Obviamente, existe uma lista e existe um dicionário. No caso comum, o dicionário está correto, mas talvez para um ou dois aplicativos você precise usar a lista.
Berin Loritsch
3
É, mas se o comportamento ainda for o mesmo, essa determinação deve estar na biblioteca. Corri através de implementações de contêineres em outros idiomas que alternarão algoritmos se for informado a priori que o número de entradas será pequeno. Concordo com sua resposta, mas depois que isso for descoberto, ele deverá estar em uma biblioteca, talvez como um SmallBidirectionalMap.
Blrfl
5
"Para o bem ou para o mal, os designers da biblioteca C # criaram um idioma para lidar com o comportamento". - Acho que você acertou na cabeça: eles "surgiram" com um idioma, em vez de usar um já amplamente utilizado, que é um tipo de retorno opcional.
Jörg W Mittag
3
@ JörgWMittag Se você está falando sobre algo parecido Option<T>, acho que isso tornaria o método muito mais difícil de usar no C # 2.0, porque não tinha correspondência de padrões.
svick 28/08
21

Algumas boas respostas aqui sobre os princípios gerais de hashtables / dicionários. Mas pensei em tocar no seu exemplo de código,

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

A partir do C # 7 (que acho que tem cerca de dois anos), isso pode ser simplificado para:

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

E é claro que poderia ser reduzido a uma linha:

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

Se você tiver um valor padrão para quando a chave não existir, ela poderá se tornar:

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

Assim, você pode obter formulários compactos usando adições de idiomas razoavelmente recentes.

David Arno
fonte
1
Boa chamada na sintaxe v7, boa opção para cortar a linha de definição extra, permitindo o uso de var +1
BrianH
2
Observe também que, se "key"não estiver presente, xserá inicializado paradefault(TValue)
Peter Duniho
Também pode ser bom como uma extensão genérica, invocada como"key".DoSomethingWithThis()
Ross Presser
Eu prefiro os designs que usam o getOrElse("key", defaultValue) objeto Null ainda é o meu padrão favorito. Trabalhe dessa maneira e você não se importa se TryGetValueretorna verdadeiro ou falso.
candied_orange
1
Eu sinto que esta resposta merece um aviso de que escrever código compacto por ser compacto é ruim. Isso pode tornar seu código muito mais difícil de ler. Além disso, se bem me lembro, TryGetValuenão é atômico / thread-safe, assim você poderia facilmente executar uma verificação de existência e outra para pegar e operar com o valor
Marie
12

Isso não é um cheiro de código nem um antipadrão, pois o uso de funções no estilo TryGet com um parâmetro out é C # idiomático. No entanto, existem 3 opções fornecidas no C # para trabalhar com um dicionário; portanto, você deve ter certeza de que está usando o correto para sua situação. Eu acho que sei de onde vem o boato de que existe um problema ao usar um parâmetro out, então vou lidar com isso no final.

Quais recursos usar ao trabalhar com um dicionário C #:

  1. Se você tem certeza de que a chave estará no dicionário, use a propriedade Item [TKey]
  2. Se a chave geralmente estiver no Dicionário, mas for ruim / raro / problemático que ela não esteja lá, use Try ... Catch para que um erro seja gerado e tente manipular o erro normalmente
  3. Se você não tiver certeza de que a chave estará no dicionário, use TryGet com o parâmetro out

Para justificar isso, basta consultar a documentação do Dictionary TryGetValue, em "Comentários" :

Este método combina a funcionalidade do método ContainsKey e a propriedade Item [TKey].

...

Use o método TryGetValue se o seu código tentar acessar frequentemente chaves que não estão no dicionário. O uso desse método é mais eficiente do que capturar a KeyNotFoundException lançada pela propriedade Item [TKey].

Este método aborda uma operação O (1).

Todo o motivo pelo qual o TryGetValue existe é atuar como uma maneira mais conveniente de usar ContainsKey e Item [TKey], evitando a pesquisa no dicionário duas vezes - portanto, fingir que não existe e fazer as duas coisas manualmente é um pouco estranho. escolha.

Na prática, raramente utilizei um Dicionário bruto, devido a essa máxima simples: escolha a classe / contêiner mais genérico que oferece a funcionalidade necessária . O dicionário não foi projetado para procurar por valor e não por chave (por exemplo); portanto, se você quiser algo que faça mais sentido, use uma estrutura alternativa. Acho que posso ter usado o Dictionary uma vez no último projeto de desenvolvimento que fiz no ano passado, simplesmente porque raramente era a ferramenta certa para o trabalho que estava tentando fazer. O dicionário certamente não é o canivete suíço da caixa de ferramentas C #.

O que há de errado com nossos parâmetros?

CA1021: Evite parâmetros

Embora os valores de retorno sejam comuns e muito usados, a aplicação correta dos parâmetros out e ref requer habilidades intermediárias de design e codificação. Os arquitetos de bibliotecas que projetam para um público em geral não devem esperar que os usuários dominem o trabalho com parâmetros out ou ref.

Acho que foi aí que você ouviu que os parâmetros eram algo como um antipadrão. Como em todas as regras, você deve ler mais de perto para entender o 'porquê' e, nesse caso, há até uma menção explícita de como o padrão Try não viola a regra :

Métodos que implementam o padrão Try, como System.Int32.TryParse, não geram essa violação.

BrianH
fonte
Toda essa lista de orientações é algo que eu gostaria que mais pessoas lessem. Para quem segue, aqui está a raiz disso. docs.microsoft.com/en-us/visualstudio/code-quality/…
Peter Wone em
10

Há pelo menos dois métodos ausentes nos dicionários de C # que, na minha opinião, limpam consideravelmente o código em várias situações em outros idiomas. O primeiro está retornando um Option, que permite escrever código como o seguinte no Scala:

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

O segundo está retornando um valor padrão especificado pelo usuário se a chave não for encontrada:

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

Há algo a ser dito para usar os idiomas que um idioma fornece quando apropriado, como o Trypadrão, mas isso não significa que você precise usar apenas o que o idioma fornece. Somos programadores. Não há problema em criar novas abstrações para facilitar a compreensão de nossa situação específica, especialmente se isso elimina muita repetição. Se você precisar frequentemente de algo, como pesquisas reversas ou iteração de valores, faça isso acontecer. Crie a interface que você deseja ter.

Karl Bielefeldt
fonte
Eu gosto da segunda opção. Talvez eu tenha que escrever um método de extensão ou dois :)
Adam B
2
No lado positivo, os métodos de extensão c # significam que você mesmo pode implementá-los
jk.
5

São 4 linhas de código para essencialmente fazer o seguinte: DoSomethingWith(dict["key"])

Eu concordo que isso é deselegante. Um mecanismo que eu gosto de usar neste caso, em que o valor é um tipo de estrutura, é:

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

Agora temos uma nova versão TryGetValuedesse retorno int?. Podemos então fazer um truque semelhante para estender T?:

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

E agora monte isso:

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

e estamos diante de uma declaração única e clara.

Ouvi dizer que o uso da palavra-chave out é um anti-padrão porque faz com que as funções alterem seus parâmetros.

Seria um pouco menos forte e diria que evitar mutações sempre que possível é uma boa idéia.

Percebo-me muitas vezes precisando de um dicionário "invertido", onde lancei as chaves e os valores.

Em seguida, implemente ou obtenha um dicionário bidirecional. Eles são fáceis de escrever ou existem muitas implementações disponíveis na internet. Existem várias implementações aqui, por exemplo:

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

Da mesma forma, muitas vezes eu gostaria de percorrer os itens de um dicionário e me encontrar convertendo chaves ou valores em listas etc. para fazer isso melhor.

Claro, todos nós fazemos.

Sinto que quase sempre há uma maneira melhor e mais elegante de usar dicionários, mas estou perdida.

Pergunte a si mesmo "suponha que eu tivesse uma classe diferente Dictionarydaquela que implementou a operação exata que gostaria de fazer; como seria essa classe?" Então, depois de responder a essa pergunta, implemente essa classe . Você é um programador de computador. Resolva seus problemas escrevendo programas de computador!

Eric Lippert
fonte
Obrigado. Você acha que poderia explicar um pouco mais o que o método de ação está realmente fazendo? Eu sou novo em ações.
Adam B
1
@AdamB uma ação é um objeto que representa a capacidade de chamar um método nulo. Como você vê, no lado do chamador, passamos o nome de um método nulo cuja lista de parâmetros corresponde aos argumentos de tipo genérico da ação. No lado do chamador, a ação é invocada como qualquer outro método.
Eric Lippert
1
@ AdamB: Você pode pensar Action<T>que é muito parecido interface IAction<T> { void Invoke(T t); }, mas com regras muito brandas sobre o que "implementa" essa interface e sobre como Invokepode ser invocado. Se você quiser saber mais, aprenda sobre "delegados" em C # e aprenda sobre expressões lambda.
Eric Lippert
Ok, eu acho que entendi. Então a ação permite que você coloque um método como o parâmetro para DoIt (). E chama esse método.
Adam B
@ AdamB: Exatamente certo. Este é um exemplo de "programação funcional com funções de ordem superior". A maioria das funções aceita dados como argumentos; funções de ordem superior tomam funções como argumentos e depois fazem coisas com essas funções. Conforme você aprende mais sobre C #, verá que o LINQ é totalmente implementado com funções de ordem superior.
Eric Lippert
4

A TryGetValue()construção é necessária apenas se você não souber se "chave" está presente como chave no dicionário ou não, caso contrário, DoSomethingWith(dict["key"])é perfeitamente válido.

Uma abordagem "menos suja" pode ser usar ContainsKey()como verificação.

Peregrino
fonte
6
Uma abordagem "menos suja" pode ser usar ContainsKey () como uma verificação. " Discordo. Por mais subótimo que TryGetValueseja, pelo menos dificulta esquecer o manuseio do estojo vazio. Idealmente, eu gostaria que isso retornasse apenas um opcional.
Alexander - Restabelecer Monica
1
Há um problema em potencial na ContainsKeyabordagem de aplicativos multithread, que é a vulnerabilidade de tempo de verificação até o tempo de uso (TOCTOU). E se algum outro thread excluir a chave entre as chamadas para ContainsKeye GetValue?
David Hammen
2
Os opcionais do @JAD compõem. Você pode ter umOptional<Optional<T>>
Alexander - Reinstate Monica
2
@DavidHammen Para ser claro, você precisaria TryGetValueon ConcurrentDictionary. O dicionário regular não seria sincronizado
Alexander - Reinstate Monica
2
@JAD Não, (o tipo opcional do Java sopra, não me inicie). Estou falando de maneira mais abstrata
Alexander - Reinstate Monica
2

Outras respostas contêm ótimos pontos, por isso não vou reafirmar aqui, mas em vez disso, vou me concentrar nessa parte, que parece ter sido amplamente ignorada até agora:

Da mesma forma, muitas vezes eu gostaria de percorrer os itens de um dicionário e me encontrar convertendo chaves ou valores em listas etc. para fazer isso melhor.

Na verdade, é muito fácil iterar em um dicionário, pois ele implementa IEnumerable:

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

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

Se você prefere o Linq, isso também funciona muito bem:

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

Em suma, não considero os dicionários um antipadrão - eles são apenas uma ferramenta específica com usos específicos.

Além disso, para obter detalhes ainda mais específicos, confira SortedDictionary(usa árvores RB para obter um desempenho mais previsível) e SortedList(que também é um dicionário de nome confuso que sacrifica a velocidade de inserção pela velocidade de pesquisa, mas brilha se você estiver olhando com um preço fixo pré- conjunto classificado). Eu tive um caso em que substituir um Dictionarypor SortedDictionaryresultou em uma ordem de magnitude de execução mais rápida (mas isso poderia acontecer de outra maneira também).

Vilx-
fonte
1

Se você acha difícil usar um dicionário, pode não ser a escolha certa para o seu problema. Os dicionários são ótimos, mas, como observou um comentarista, geralmente são usados ​​como atalhos para algo que deveria ter sido uma aula. Ou o próprio dicionário pode estar certo como método de armazenamento principal, mas deve haver uma classe de wrapper para fornecer os métodos de serviço desejados.

Muito se tem falado sobre Dict [chave] versus TryGet. O que eu uso muito é a iteração no dicionário usando o KeyValuePair. Aparentemente, esse é um construto menos conhecido.

O principal benefício de um dicionário é que ele é realmente rápido em comparação com outras coleções, à medida que o número de itens aumenta. Se você precisar verificar se existe uma chave muito, você pode se perguntar se o uso de um dicionário é apropriado. Como cliente, você normalmente deve saber o que coloca lá e, portanto, o que seria seguro consultar.

Martin Maat
fonte