Menu

Dictionary em C#: Add, TryGetValue, percorrer e ordenar

Dictionary<TKey, TValue> mapeia chaves para valores com busca rápida. Veja como adicionar e atualizar entradas, ler com segurança usando TryGetValue, percorrer entradas KeyValuePair, contar ocorrências, ignorar maiúsculas e minúsculas nas chaves e manter as chaves ordenadas com SortedDictionary.

Esta página tem editores executáveis - edite, execute e veja a saída na hora.

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

TarefaCódigo
Criarnew Dictionary<string, int>()
Inserir ou sobrescreverd[k] = v
Inserir, lançar exceção se duplicadod.Add(k, v)
Inserir só se for novad.TryAdd(k, v)
Ler, lançar exceção se ausented[k]
Ler com segurançad.TryGetValue(k, out var v)
A chave existed.ContainsKey(k)
Removerd.Remove(k) (retorna bool)
Tamanhod.Count
Chaves, valoresd.Keys, d.Values
Ordenado pela chaved.OrderBy(p => p.Key) ou SortedDictionary
Ignorar maiúsculas e minúsculasnew Dictionary<string, T>(StringComparer.OrdinalIgnoreCase)

Erros comuns

  • Ler uma chave ausente com d[k]. Lança KeyNotFoundException; use TryGetValue.
  • Chamar Add para uma chave que pode existir. Lança ArgumentException; use o indexador ou TryAdd.
  • Adicionar chaves dentro de um foreach sobre o dicionário. Lança InvalidOperationException; 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.
  • ContainsKey e depois o indexador. Duas consultas; TryGetValue faz 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).

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR