Dictionary<TKey, TValue> guarda valores sob chaves únicas e encontra um valor pela chave em tempo praticamente constante, não importa quantas entradas existam. É o equivalente em C# a um hash map: uma agenda de nome para número, um cache de ID para registro, uma contagem por palavra.
Criando um dicionário e lendo valores
Saída:
12
2.50
2
True
False
As duas formas de inicialização fazem a mesma coisa. A forma ["key"] = value (C# 6) usa o indexador, então uma chave repetida sobrescreve; a forma { key, value } chama Add, então uma chave repetida lança exceção quando a linha executa.
ContainsKey é uma consulta por hash e é rápida. ContainsValue precisa varrer todas as entradas, porque os valores não são indexados.
Add vs o indexador vs TryAdd
Há três formas de colocar uma entrada, e elas só diferem no que acontece quando a chave já existe:
Saída:
26
Caught ArgumentException
True
False
31
Add lançar exceção em uma duplicata é uma vantagem: revela dados que deveriam ser únicos e não eram. Use o indexador quando você quer dizer "inserir ou atualizar", e TryAdd (.NET Core 2.0 em diante) quando o primeiro valor deve prevalecer.
Chaves não podem ser null. Add(null, ...) ou dict[null] lança ArgumentNullException. Valores podem ser null quando o tipo do valor permite.
KeyNotFoundException e TryGetValue
Ler com o indexador uma chave que não existe lança KeyNotFoundException. É o erro de dicionário mais comum, e a solução quase sempre é TryGetValue.
Saída:
Caught KeyNotFoundException
Found ana@example.com
Missing, value is null: True
no email
TryGetValue faz uma consulta por hash e informa o sucesso como um bool. O padrão if (dict.ContainsKey(k)) { var v = dict[k]; } funciona, mas consulta a chave duas vezes. Quando a chave não existe, a variável out recebe o valor padrão do tipo (null, 0, false).
No .NET Core 2.0 em diante também existe GetValueOrDefault(key, fallback), que retorna o valor alternativo quando a chave não existe: emails.GetValueOrDefault(103, "no email").
Atualizando e removendo entradas
Saída:
2
True
False
1
0
cart["milk"] += 1 lança KeyNotFoundException se milk ainda não estiver no dicionário, porque ele lê antes de escrever. Remove retorna false em vez de lançar exceção para uma chave ausente, então não é preciso verificar ContainsKey antes.
Percorrendo: KeyValuePair, Keys e Values
Um foreach sobre um dicionário produz itens KeyValuePair<TKey, TValue>, cada um com um Key e um Value.
Saída:
Ana: 88
Ben: 72
Chloe: 95
Ana Ben Chloe
Total 255
77
pair.Value é somente leitura, então atualizar valores significa escrever pelo indexador. O último laço percorre uma cópia List<string> das chaves, o que é sempre seguro; percorrer scores.Keys diretamente enquanto sobrescreve valores existentes é permitido no .NET Core 3.0 em diante, mas lança InvalidOperationException no .NET Framework.
Adicionar uma chave nova dentro de um foreach sobre o mesmo dicionário lança InvalidOperationException em todas as versões. Remover durante a enumeração lança exceção no .NET Framework e é permitido a partir do .NET Core 3.0. Código que precisa rodar em qualquer lugar junta primeiro as chaves a remover e as remove depois do laço.
Com C# 7 e .NET Core 2.0 ou posterior, o KeyValuePair pode ser desconstruído no cabeçalho do laço:
foreach (var (name, score) in scores)
{
Console.WriteLine($"{name}: {score}");
}
Contando com um dicionário
Contar ocorrências é o uso clássico. Leia a contagem atual com TryGetValue (uma chave ausente dá 0) e escreva de volta.
Saída:
the 3
cat 1
and 2
dog 1
bird 1
A mesma forma serve para agrupar itens: Dictionary<string, List<Order>>, em que você busca a lista com TryGetValue, cria e guarda uma se não existir, e então faz Add nela. Para contar e agrupar pontualmente, o LINQ faz isso em uma expressão: words.GroupBy(w => w).ToDictionary(g => g.Key, g => g.Count()). Veja LINQ.
Chaves sem diferenciar maiúsculas e minúsculas com um comparador
Por padrão, chaves string são comparadas de forma exata: "Apple" e "apple" são duas chaves. Passe um IEqualityComparer<string> ao construtor para mudar isso.
Saída:
False
text/html
1
StringComparer.OrdinalIgnoreCase é a escolha certa para identificadores como cabeçalhos HTTP, extensões de arquivo e nomes de usuário. Chamar .ToLower() em cada chave antes de guardá-la também funciona, mas é fácil esquecer em algum lugar.
Para chaves de uma classe sua, o dicionário chama o GetHashCode e o Equals da chave. Uma classe que não os sobrescreve compara por referência, então dois objetos separados com os mesmos campos são chaves diferentes. Veja HashSet para saber como escrever esse par.
Ordem, ordenação e SortedDictionary
Um Dictionary não promete nenhuma ordem de enumeração. Na prática, um dicionário que só recebeu adições é enumerado na ordem de inserção, mas depois de um Remove, um Add posterior pode reutilizar a posição liberada e aparecer antes. O código nunca deve depender disso.
Quando você precisa de uma ordem, ordene no ponto de uso ou use uma coleção ordenada:
Saída:
Cairo 210
Lima 340
Oslo 520
By value, highest first:
Oslo 520
Lima 340
Cairo 210
Berlin, Cairo, Lima, Oslo
SortedDictionary<TKey, TValue> mantém as chaves sempre ordenadas (é uma árvore balanceada), então consultas e inserções são O(log n) em vez de O(1). Use-o quando você enumera pela ordem das chaves com frequência; ordene um dicionário normal com LINQ quando só precisa da ordem uma vez. SortedList<TKey, TValue> é uma terceira opção, que usa menos memória mas é lenta para inserir quando fica grande.
Referência rápida
| Tarefa | Código |
|---|---|
| Criar | new Dictionary<string, int>() |
| Inserir ou sobrescrever | d[k] = v |
| Inserir, lançar exceção se duplicado | d.Add(k, v) |
| Inserir só se for nova | d.TryAdd(k, v) |
| Ler, lançar exceção se ausente | d[k] |
| Ler com segurança | d.TryGetValue(k, out var v) |
| A chave existe | d.ContainsKey(k) |
| Remover | d.Remove(k) (retorna bool) |
| Tamanho | d.Count |
| Chaves, valores | d.Keys, d.Values |
| Ordenado pela chave | d.OrderBy(p => p.Key) ou SortedDictionary |
| Ignorar maiúsculas e minúsculas | new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase) |
Erros comuns
- Ler uma chave ausente com
d[k]. LançaKeyNotFoundException; useTryGetValue. - Chamar
Addpara uma chave que pode existir. LançaArgumentException; use o indexador ouTryAdd. - Adicionar chaves dentro de um
foreachsobre o dicionário. LançaInvalidOperationException; junte as mudanças e aplique depois. - Depender da ordem de enumeração. Ordene, ou use
SortedDictionary. - Mudar os campos de um objeto chave depois de inseri-lo. O hash code muda e a entrada não pode mais ser encontrada.
ContainsKeye depois o indexador. Duas consultas;TryGetValuefaz uma.
Perguntas frequentes
Qual a diferença entre Dictionary.Add e o indexador em C#?
dict.Add(key, value) insere uma entrada nova e lança ArgumentException se a chave já existir. dict[key] = value insere a entrada se a chave for nova e sobrescreve o valor se ela existir, e nunca lança exceção por duplicidade. TryAdd(key, value) insere só quando a chave é nova e retorna false caso contrário.
Como funciona o TryGetValue em C#?
dict.TryGetValue(key, out var value) retorna true e define value quando a chave existe, e retorna false e define value com o padrão do tipo quando não existe. Ele faz uma consulta, enquanto ContainsKey seguido de dict[key] faz duas, e nunca lança KeyNotFoundException.
Como percorrer um Dictionary em C#?
foreach (KeyValuePair<string, int> pair in dict) entrega cada entrada com pair.Key e pair.Value. Para percorrer só as chaves ou só os valores, use dict.Keys ou dict.Values. Não adicione chaves ao dicionário dentro desse laço: isso lança InvalidOperationException.
Um Dictionary em C# é ordenado?
Nenhuma ordem é garantida. Um dicionário que só recebeu adições costuma ser enumerado na ordem de inserção, mas depois de um Remove novas entradas podem ocupar a posição liberada, e a ordem muda. Ordene quando precisar de uma ordem: dict.OrderBy(p => p.Key), ou use SortedDictionary<TKey, TValue>, que sempre enumera pela chave.
Como fazer as chaves de um Dictionary ignorarem maiúsculas e minúsculas?
Passe um comparador ao construtor: new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase). Aí "Apple" e "apple" são a mesma chave para consultas, Add e ContainsKey. O comparador é fixado quando o dicionário é criado.
O que é um KeyValuePair em C#?
KeyValuePair<TKey, TValue> é a struct que um dicionário entrega para cada entrada quando você o percorre. Ela tem as propriedades somente leitura Key e Value, então você não pode mudar uma entrada por meio dela; escreva dict[pair.Key] = newValue (depois do laço, ou sobre uma cópia das chaves).