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.
System.Collections.Generic
namespace não são seguras para threads .Dictionary
, o que eles realmente queriam era uma nova classe. Para aqueles 50% (apenas), é um cheiro de design.Respostas:
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
null
ounil
ou 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
Try
variante. É a mesma abordagem usada para analisar seqüências de caracteres em números inteiros ou objetos de data / hora. Essencialmente, o impacto é assim:E isso foi transferido para o dicionário, cujo indexador delega para
Get(index)
:É assim que o idioma é projetado.
As
out
variá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
out
variá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.
fonte
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.Algumas boas respostas aqui sobre os princípios gerais de hashtables / dicionários. Mas pensei em tocar no seu exemplo de código,
A partir do C # 7 (que acho que tem cerca de dois anos), isso pode ser simplificado para:
E é claro que poderia ser reduzido a uma linha:
Se você tiver um valor padrão para quando a chave não existir, ela poderá se tornar:
Assim, você pode obter formulários compactos usando adições de idiomas razoavelmente recentes.
fonte
"key"
não estiver presente,x
será inicializado paradefault(TValue)
"key".DoSomethingWithThis()
getOrElse("key", defaultValue)
objeto Null ainda é o meu padrão favorito. Trabalhe dessa maneira e você não se importa seTryGetValue
retorna verdadeiro ou falso.TryGetValue
não é atômico / thread-safe, assim você poderia facilmente executar uma verificação de existência e outra para pegar e operar com o valorIsso 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 #:
Para justificar isso, basta consultar a documentação do Dictionary TryGetValue, em "Comentários" :
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
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 :
fonte
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:O segundo está retornando um valor padrão especificado pelo usuário se a chave não for encontrada:
Há algo a ser dito para usar os idiomas que um idioma fornece quando apropriado, como o
Try
padrã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.fonte
Eu concordo que isso é deselegante. Um mecanismo que eu gosto de usar neste caso, em que o valor é um tipo de estrutura, é:
Agora temos uma nova versão
TryGetValue
desse retornoint?
. Podemos então fazer um truque semelhante para estenderT?
:E agora monte isso:
e estamos diante de uma declaração única e clara.
Seria um pouco menos forte e diria que evitar mutações sempre que possível é uma boa idéia.
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
Claro, todos nós fazemos.
Pergunte a si mesmo "suponha que eu tivesse uma classe diferente
Dictionary
daquela 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!fonte
Action<T>
que é muito parecidointerface IAction<T> { void Invoke(T t); }
, mas com regras muito brandas sobre o que "implementa" essa interface e sobre comoInvoke
pode ser invocado. Se você quiser saber mais, aprenda sobre "delegados" em C # e aprenda sobre expressões lambda.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.fonte
TryGetValue
seja, pelo menos dificulta esquecer o manuseio do estojo vazio. Idealmente, eu gostaria que isso retornasse apenas um opcional.ContainsKey
abordagem 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 paraContainsKey
eGetValue
?Optional<Optional<T>>
TryGetValue
onConcurrentDictionary
. O dicionário regular não seria sincronizadoOutras 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:
Na verdade, é muito fácil iterar em um dicionário, pois ele implementa
IEnumerable
:Se você prefere o Linq, isso também funciona muito bem:
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) eSortedList
(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 umDictionary
porSortedDictionary
resultou em uma ordem de magnitude de execução mais rápida (mas isso poderia acontecer de outra maneira também).fonte
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.
fonte