Dictionary e HashSet: Coleções Especializadas

Dictionary e HashSet: Coleções Especializadas

Duas coleções especializadas que completam o repertório: o Dictionary, que localiza um valor pela chave sem percorrer nada, e o HashSet, que descarta repetições automaticamente. A aula inclui o cuidado com chaves inexistentes e um guia prático para escolher entre List, Dictionary e HashSet.
Linguagem C#

• • 11 min de leitura

Na Fase 4, conhecemos duas coleções: o array e a List. Ambas guardam uma sequência de valores, acessados por sua posição numérica. Isso resolve muitos problemas, mas não todos. Considere: você tem uma agenda telefônica e quer, dado um nome, encontrar rapidamente o telefone correspondente. Numa List, você teria de percorrer todos os contatos procurando o nome — lento, se a lista for grande. Ou: você quer manter um registro de quais páginas de um site já foram visitadas, garantindo que nenhuma se repita. Numa List, teria de verificar manualmente, a cada adição, se o item já existe. Para esses problemas, o C# oferece coleções especializadas que os resolvem com elegância: o Dictionary, que associa chaves a valores para busca instantânea, e o HashSet, que guarda conjuntos sem repetições. Esta aula os apresenta, ampliando seu repertório de organização de dados.

O Dictionary: busca por chave, não por posição

O Dictionary (dicionário) é uma coleção que associa chaves a valores. Em vez de acessar elementos por uma posição numérica (como no array e na List), você os acessa por uma chave de sua escolha — que pode ser um texto, um número, ou outro tipo. A analogia que dá nome à estrutura é perfeita: num dicionário de idiomas, você não procura uma definição pela "posição" dela; procura pela palavra (a chave), e obtém a definição (o valor). O Dictionary funciona assim, e sua grande virtude é que essa busca por chave é praticamente instantânea, independentemente do tamanho da coleção.

Criamos um Dictionary especificando dois tipos entre os sinais de menor e maior: o tipo da chave e o tipo do valor. Veja uma agenda que associa nomes (texto) a idades (número):

// Dictionary<TipoDaChave, TipoDoValor> — aqui: chave texto, valor número.
Dictionary<string, int> idades = new();

// Adicionar associações: a chave entre colchetes, o valor à direita.
idades["Ana"] = 30;
idades["Beto"] = 25;
idades["Carla"] = 42;

// Buscar por chave — instantâneo, não importa o tamanho:
Console.WriteLine(idades["Ana"]);   // 30

O Dictionary<string, int> significa "um dicionário cujas chaves são textos e cujos valores são números". Adicionamos associações com a sintaxe idades["Ana"] = 30, que se lê "associe à chave 'Ana' o valor 30". E buscamos com idades["Ana"], que devolve o valor associado àquela chave. Repare como isso resolve o problema da agenda: dado o nome, obtemos a idade diretamente, sem percorrer nada. Cada chave é única — associar um novo valor a uma chave existente substitui o valor anterior, não cria uma segunda entrada.

O cuidado com chaves inexistentes

Há uma armadilha importante no Dictionary que reencontra uma lição antiga. Se você tentar buscar uma chave que não existe, o programa quebra:

// Console.WriteLine(idades["Daniel"]);   // QUEBRA: a chave "Daniel" não existe

Isso lembra o erro de acessar uma posição inexistente de um array (Aula #19), e a solução segue o mesmo espírito da conversão segura que aprendemos com TryParse (Aula #08): existe um método TryGetValue que tenta buscar a chave sem quebrar, informando se ela existe:

if (idades.TryGetValue("Daniel", out int idadeDaniel))
{
    Console.WriteLine($"Daniel tem {idadeDaniel} anos.");
}
else
{
    Console.WriteLine("Daniel não está na agenda.");
}

O TryGetValue devolve verdadeiro se a chave existe (colocando o valor encontrado na variável de saída idadeDaniel, via o out que vimos no TryParse) e falso se não existe — permitindo tratar a ausência sem quebrar. Há também o método ContainsKey, que apenas verifica se uma chave está presente, devolvendo verdadeiro ou falso. A regra prática, no mesmo espírito do curso: quando não tiver certeza de que uma chave existe, prefira TryGetValue ou ContainsKey ao acesso direto por colchetes.

Percorrendo um Dictionary

Você também pode percorrer todas as associações de um dicionário com foreach. Cada item de um Dictionary, ao ser percorrido, é um par que contém uma chave e um valor, acessíveis por .Key e .Value:

foreach (var par in idades)
{
    Console.WriteLine($"{par.Key} tem {par.Value} anos.");
}

Aqui, par.Key é a chave (o nome) e par.Value é o valor (a idade) de cada associação. (Usamos var — a inferência de tipo da Aula #06 — porque o tipo de cada par tem um nome um pouco longo, e var deixa o C# inferi-lo.) Percorrer o dicionário assim é útil para exibir ou processar todas as associações. O Dictionary, portanto, oferece o melhor dos dois mundos: busca instantânea por chave e a possibilidade de percorrer tudo quando necessário.

O HashSet: um conjunto sem repetições

A segunda coleção especializada é o HashSet (conjunto), e ela resolve um problema diferente: manter uma coleção de itens únicos, sem repetições, com verificação instantânea de pertencimento. Diferentemente da List, que aceita valores repetidos e cuja verificação de "contém" percorre os elementos, o HashSet automaticamente ignora duplicatas e verifica presença de forma quase imediata. É o conjunto matemático: cada elemento aparece no máximo uma vez.

HashSet<string> paginasVisitadas = new();

Console.WriteLine(paginasVisitadas.Add("home"));    // True — adicionou
Console.WriteLine(paginasVisitadas.Add("sobre"));   // True — adicionou
Console.WriteLine(paginasVisitadas.Add("home"));    // False — JÁ existia, ignorou

Console.WriteLine(paginasVisitadas.Count);          // 2 — a duplicata não entrou
Console.WriteLine(paginasVisitadas.Contains("home")); // True — verificação rápida

Repare em dois comportamentos. Primeiro, o método Add devolve verdadeiro se o item foi realmente adicionado, e falso se ele já existia — útil para detectar duplicatas de passagem. Segundo, adicionar "home" pela segunda vez não teve efeito (a Count permaneceu 2), pois o conjunto ignora repetições automaticamente. E o Contains verifica pertencimento instantaneamente. Isso resolve o problema das páginas visitadas com elegância: cada página que você tenta adicionar ou entra (se nova) ou é ignorada (se repetida), e verificar se uma página já foi vista é imediato — sem percorrer nada, sem verificação manual.

Qual coleção escolher: um guia

Agora você conhece quatro coleções, e escolher bem entre elas é parte de programar com clareza. Um guia prático: use a List (ou o array) quando precisar de uma sequência ordenada de itens, acessados por posição, possivelmente com repetições — o caso mais comum e geral. Use o Dictionary quando precisar associar identificadores a valores e buscar por esses identificadores rapidamente — uma agenda, um cadastro por código, qualquer "dado X, encontre o Y correspondente". Use o HashSet quando precisar apenas de um conjunto de itens únicos, para verificar pertencimento ou evitar duplicatas, sem valores associados nem ordem importante. A pergunta que orienta a escolha: "como vou acessar esses dados — por posição, por uma chave, ou apenas verificando presença?". A resposta aponta a coleção certa, e escolhê-la bem torna seu código mais simples e eficiente.

O repertório de coleções ficou completo para a maioria das necessidades reais. O Dictionary guarda pares de chave e valor e localiza um item pela chave de forma imediata, sem percorrer nada, o que o torna a estrutura certa quando a busca é por identificador e não por posição. O HashSet guarda elementos únicos e descarta repetições automaticamente.

A escolha entre as estruturas deixou de ser arbitrária: List para sequência ordenada com repetições, Dictionary para busca por chave, HashSet para unicidade. Escolher bem a coleção costuma resolver, de saída, problemas que seriam trabalhosos de contornar depois — e o cuidado com chaves inexistentes ficou registrado como a armadilha própria do Dictionary.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Crie um Dictionary<string, decimal> que associe nomes de produtos a preços. Adicione quatro produtos, e depois busque e exiba o preço de um deles pela chave. Em seguida, tente buscar um produto que não existe usando TryGetValue, tratando a ausência com uma mensagem apropriada.

Ver resposta

✓ Resposta: Exemplo:

Dictionary<string, decimal> precos = new();
precos["Caderno"] = 12.5m;
precos["Caneta"] = 2m;
precos["Borracha"] = 1.5m;
precos["Régua"] = 3m;
Console.WriteLine($"Caderno: R$ {precos["Caderno"]}");   // Caderno: R$ 12,5
if (precos.TryGetValue("Mochila", out decimal preco))
    Console.WriteLine($"Mochila: R$ {preco}");
else
    Console.WriteLine("Mochila não está no catálogo.");

A busca por chave é direta; o TryGetValue trata a chave inexistente ("Mochila") sem quebrar, caindo no else.

Exercício 2

Explique, com suas palavras, a diferença fundamental entre como se acessa um elemento numa List e num Dictionary. Por que o Dictionary é mais apropriado para uma agenda telefônica (buscar telefone pelo nome) do que uma List?

Ver resposta

✓ Resposta: Numa List, um elemento é acessado por sua posição numérica (um índice, como lista[0]), e encontrar um valor específico exige percorrer os elementos. Num Dictionary, um elemento é acessado por uma chave de sua escolha (como dicionario["Ana"]), diretamente, sem percorrer. O Dictionary é mais apropriado para uma agenda telefônica porque queremos buscar o telefone pelo nome — associando cada nome (chave) a um telefone (valor) e obtendo o telefone instantaneamente pela chave —, enquanto numa List teríamos de percorrer todos os contatos procurando o nome, o que é lento em listas grandes. A busca por chave do Dictionary é praticamente instantânea, independentemente do tamanho.

Exercício 3

Crie um HashSet<int> e tente adicionar os números 1, 2, 2, 3, 3, 3. Depois, exiba a Count do conjunto e explique por que ela não é 6. Use o valor de retorno de Add para detectar e relatar quando uma tentativa de adição foi de um número repetido.

Ver resposta

✓ Resposta: Exemplo:

HashSet<int> numeros = new();
int[] tentativas = { 1, 2, 2, 3, 3, 3 };
foreach (int n in tentativas)
{
    if (!numeros.Add(n))
        Console.WriteLine($"{n} já estava no conjunto (repetido).");
}
Console.WriteLine(numeros.Count);   // 3

A Count é 3, não 6, porque o HashSet ignora duplicatas automaticamente: apenas os valores distintos (1, 2, 3) entram, e as repetições são descartadas. O Add devolve falso quando o número já existia, o que permite detectar e relatar cada tentativa repetida.

Exercício 4

Você tem uma List<string> com nomes que podem conter repetições. Explique como você usaria um HashSet para descobrir quantos nomes distintos existem na lista, e por que o HashSet torna isso fácil.

Ver resposta

✓ Resposta: Para descobrir quantos nomes distintos existem numa List<string> com repetições, eu criaria um HashSet<string> a partir da lista — por exemplo, adicionando cada nome da lista ao conjunto (ou construindo o conjunto diretamente com base na lista). Como o HashSet ignora duplicatas automaticamente, apenas os nomes distintos permaneceriam nele, e a sua Count daria a quantidade de nomes únicos. O HashSet torna isso fácil porque a eliminação de repetições é a sua característica natural: não é preciso escrever nenhuma verificação manual de "este nome já apareceu?"; basta jogar todos no conjunto e ler a contagem.

Exercício 5

Sem código: para cada situação, escolha entre List, Dictionary e HashSet, justificando: (a) os itens de um carrinho de compras, na ordem em que foram adicionados; (b) um sistema que, dado o número de matrícula de um aluno, retorna seus dados; (c) o controle de quais e-mails já foram processados, para não processar o mesmo duas vezes.

Ver resposta

✓ Resposta: (a) List — o carrinho é uma sequência ordenada, que importa a ordem de adição e pode ter itens repetidos; o acesso é por posição/ordem. (b) Dictionary — o sistema associa uma chave (número de matrícula) a um valor (os dados do aluno) e busca por essa chave; é o caso clássico de "dado o identificador, encontre o valor correspondente". (c) HashSet — precisa-se apenas saber se um e-mail já foi processado, sem valor associado e sem permitir repetição, e a verificação de pertencimento deve ser rápida; o conjunto de itens únicos é ideal para evitar processar o mesmo e-mail duas vezes.

Comentários

Mais em Linguagem C#

Um Blog Completo com ASP.NET Core
Um Blog Completo com ASP.NET Core

Um site navegável completo em C#: um blog com Razor Pages, com listagem de…

Construtores: Objetos que Nascem Completos
Construtores: Objetos que Nascem Completos

O construtor, método especial que roda na criação do objeto e permite exigir…

IEnumerable: O que Torna Algo "Percorrível"
IEnumerable: O que Torna Algo "Percorrível"

Por que arrays, listas, dicionários e conjuntos aceitam todos o mesmo foreach…