C# HashSet<T>
Às vezes você não quer uma lista, quer um conjunto: as figurinhas que a Ana já tem, os nomes de quem confirmou presença, as palavras que já apareceram num texto. Num conjunto, cada coisa aparece uma vez só, e a pergunta mais importante é "isto está aqui?". O HashSet<T> responde essa pergunta num instante, mesmo com milhões de itens, e ainda faz as contas de conjuntos da escola: união, interseção e diferença.
- O que é um conjunto e como criar um
HashSet<T>. Add(e oboolque ele devolve),Contains,RemoveeCount.- Tirar os repetidos de uma lista em uma linha.
- Por que a ordem não é garantida, e o
SortedSet<T>. - União, interseção e diferença:
UnionWith,IntersectWith,ExceptWith,SymmetricExceptWith. - Perguntas de conjunto:
IsSubsetOf,Overlaps,SetEquals. - Conjuntos de objetos seus: por que uma
classdeixa entrar "repetidos" e umrecordnão. - Os erros reais:
CS0021(o conjunto não tem posições) e aInvalidOperationExceptiondoforeach.
Todos os programas desta aula rodaram com o .NET 10 na máquina da Ana (usuária ana, computador notebook), na pasta ~/cs, com dotnet run arquivo.cs. Você pode repetir cada um no editor do guia.
A ideia: um álbum de figurinhas
A Ana coleciona figurinhas. No álbum, cada figurinha tem um lugar só: colar a figurinha 7 de novo não cria uma segunda 7, ela vai para o monte de repetidas. E, quando um amigo oferece uma figurinha, a primeira pergunta é: "eu já tenho essa?".
Um HashSet<T> ("conjunto por hash") é esse álbum:
- Não aceita repetidos. Tentar acrescentar um item que já está lá não faz nada (e ele avisa que não fez).
- Não tem posições. Não existe "a figurinha da posição 0". Existe só "tem" ou "não tem".
- Responde "tem?" muito rápido. Numa List, o
Containsolha item por item; numHashSet, ele vai direto ao lugar certo, como o Dictionary faz com as chaves.
O T em HashSet<T> é o tipo dos itens, escolhido por você: HashSet<int> guarda números; HashSet<string>, textos (veja Generics).
1. Criar, acrescentar e perguntar
var album = new HashSet<int>();
Console.WriteLine(album.Add(7));
Console.WriteLine(album.Add(12));
Console.WriteLine(album.Add(7));
album.Add(30);
Console.WriteLine($"Figurinhas diferentes: {album.Count}");
Console.WriteLine(album.Contains(12));
Console.WriteLine(album.Contains(99));
Console.WriteLine(album.Remove(12));
Console.WriteLine(album.Remove(12));
Console.WriteLine($"Figurinhas diferentes: {album.Count}");
Console.WriteLine(album);
$ dotnet run album.cs True « a 7 entrou True « a 12 entrou False « a 7 já estava: não entrou de novo Figurinhas diferentes: 3 « 7, 12 e 30 True False True « tirou a 12 False « a 12 já não estava lá Figurinhas diferentes: 2 System.Collections.Generic.HashSet`1[System.Int32]
- new HashSet<int>()
- Cria um conjunto vazio de números inteiros.
- album.Add(7)
- Add = acrescentar. Devolve um
bool:truese o item entrou,falsese ele já estava no conjunto. Diferente doAdddo Dictionary, aqui um repetido não lança exceção. - album.Add(30);
- Você pode ignorar o
boolquando não precisa dele. - album.Count
- Quantos itens diferentes o conjunto tem.
- album.Contains(12)
- Contains = contém. "A 12 está no álbum?" É a pergunta que o
HashSetresponde mais rápido. - album.Remove(12)
- Tira o item. Devolve
truese ele estava lá efalsese não estava (sem exceção). - Console.WriteLine(album)
- Como o
Dictionary, o conjunto não mostra o conteúdo: aparece só o nome do tipo. Para ver os itens, useforeachoustring.Join.
O bool do Add trabalha para você
Como o Add já diz se o item é novo, você não precisa de um Contains antes. Este laço acha as figurinhas repetidas de um pacote numa passada só:
int[] pacote = [5, 18, 5, 40, 18, 5];
var album = new HashSet<int>();
foreach (int fig in pacote)
{
if (!album.Add(fig))
Console.WriteLine($"Repetida: {fig}");
}
Console.WriteLine($"Coladas: {album.Count}");
$ dotnet run pacote.cs Repetida: 5 Repetida: 18 Repetida: 5 Coladas: 3
Leia if (!album.Add(fig)) como "se não deu para colar". O ! é o "não" de Operadores Lógicos. O Add sempre tenta colar; quando devolve false, a figurinha era repetida.
Criar já com itens
Dá para criar o conjunto com itens entre chaves (o inicializador) ou a partir de outra coleção. Repetidos são ignorados nas duas formas:
var cores = new HashSet<string> { "azul", "verde", "azul" };
int[] numeros = [3, 1, 3, 2];
var semRepetir = new HashSet<int>(numeros);
HashSet<char> vogais = ['a', 'e', 'i', 'o', 'u'];
A última linha usa a expressão de coleção [ … ] C# 12, que também funciona com HashSet quando o tipo está escrito à esquerda.
2. Tirar os repetidos de uma lista
A Ana anotou quem confirmou presença na festa, mas algumas pessoas confirmaram duas vezes. Quantas pessoas vêm de verdade?
string[] convidados = ["Ana", "Bruno", "Ana", "Carla", "Bruno", "Ana"];
var semRepetir = new HashSet<string>(convidados);
Console.WriteLine($"Nomes na lista: {convidados.Length}");
Console.WriteLine($"Pessoas diferentes: {semRepetir.Count}");
foreach (string nome in semRepetir)
{
Console.WriteLine(nome);
}
$ dotnet run repetidos.cs Nomes na lista: 6 Pessoas diferentes: 3 Ana Bruno Carla
A linha new HashSet<string>(convidados) passa todos os nomes do array para o conjunto, e os repetidos simplesmente não entram. O foreach percorre o conjunto como percorreria uma lista.
Missão: a agenda da Ana tem telefones repetidos. O programa conta 7. Use um HashSet<string> para contar só os diferentes e mostrar Telefones diferentes: 4.
string[] telefones = ["99001-0001", "99001-0002", "99001-0001", "99001-0003",
"99001-0002", "99001-0004", "99001-0001"];
Console.WriteLine($"Telefones diferentes: {telefones.Length}");
string[] telefones = ["99001-0001", "99001-0002", "99001-0001", "99001-0003",
"99001-0002", "99001-0004", "99001-0001"];
var diferentes = new HashSet<string>(telefones);
Console.WriteLine($"Telefones diferentes: {diferentes.Count}");
A ordem de um HashSet
Os convidados saíram na ordem em que foram anotados, mas não conte com isso. O HashSet não promete nenhuma ordem: ele guarda cada item no lugar que o hash code do item indica (uma "impressão digital" numérica; veja A Classe object), e, quando um item sai, o próximo que entra pode ocupar o lugar vago:
var cores = new HashSet<string> { "azul", "verde", "vermelho", "amarelo" };
Console.WriteLine(string.Join(", ", cores));
cores.Remove("verde");
cores.Add("roxo");
Console.WriteLine(string.Join(", ", cores));
var ordenado = new SortedSet<string>(cores);
Console.WriteLine(string.Join(", ", ordenado));
$ dotnet run ordem.cs azul, verde, vermelho, amarelo azul, roxo, vermelho, amarelo « o roxo entrou por último, mas ficou no lugar do verde amarelo, azul, roxo, vermelho « SortedSet: sempre em ordem
HashSet
Se a ordem importa, use um SortedSet<T> (que mantém os itens sempre ordenados e tem os mesmos métodos Add, Contains, Remove…) ou ordene na hora de mostrar, com o Order() do LINQ: string.Join(", ", cores.Order()). O SortedSet é um pouco mais lento que o HashSet; use-o só quando precisar da ordem.
3. Operações de conjunto
Lembra dos diagramas de Venn da escola, com dois círculos que se cruzam? O HashSet faz essas contas. A Ana e o Bruno escolheram matérias para a monitoria:
Ana: Matemática Física Química Bruno: Física Química História
var ana = new HashSet<string> { "Matemática", "Física", "Química" };
var bruno = new HashSet<string> { "Física", "História", "Química" };
var juntas = new HashSet<string>(ana);
juntas.UnionWith(bruno);
Console.WriteLine($"União: {string.Join(", ", juntas.Order())}");
var comum = new HashSet<string>(ana);
comum.IntersectWith(bruno);
Console.WriteLine($"Interseção: {string.Join(", ", comum.Order())}");
var soAna = new HashSet<string>(ana);
soAna.ExceptWith(bruno);
Console.WriteLine($"Só a Ana: {string.Join(", ", soAna.Order())}");
var soUm = new HashSet<string>(ana);
soUm.SymmetricExceptWith(bruno);
Console.WriteLine($"Só um dos dois: {string.Join(", ", soUm.Order())}");
Console.WriteLine($"ana continua com {ana.Count}");
$ dotnet run operacoes.cs União: Física, História, Matemática, Química Interseção: Física, Química Só a Ana: Matemática Só um dos dois: História, Matemática ana continua com 3
| Método | Tradução | Fica com… | Na escola |
|---|---|---|---|
a.UnionWith(b) | "unir com" | tudo o que está em a ou em b | A ∪ B |
a.IntersectWith(b) | "cruzar com" | só o que está em a e em b | A ∩ B |
a.ExceptWith(b) | "tirar o que tem em" | o que está em a e não está em b | A − B |
a.SymmetricExceptWith(b) | "diferença simétrica" | o que está em um só dos dois | A Δ B |
a.UnionWith(b) não devolve um conjunto novo: ele altera o a (e devolve void, nada). Por isso o programa copia a Ana antes de cada conta (new HashSet<string>(ana)), e no fim o conjunto ana continua com 3 matérias. Se você fizer ana.IntersectWith(bruno) direto, perde a Matemática da Ana.
Perguntas sobre dois conjuntos
var turma = new HashSet<string> { "Ana", "Bruno", "Carla", "Davi" };
var time = new HashSet<string> { "Ana", "Carla" };
var outros = new HashSet<string> { "Eva", "Fábio" };
Console.WriteLine(time.IsSubsetOf(turma));
Console.WriteLine(turma.IsSupersetOf(time));
Console.WriteLine(turma.Overlaps(outros));
Console.WriteLine(time.SetEquals(new[] { "Carla", "Ana", "Ana" }));
$ dotnet run perguntas.cs True « todo mundo do time está na turma True « a turma contém o time False « a turma e os outros não têm ninguém em comum True « os mesmos itens; a ordem e os repetidos não contam
| Método | Pergunta |
|---|---|
a.IsSubsetOf(b) | "Todo item de a está em b?" (subconjunto) |
a.IsSupersetOf(b) | "a contém todos os itens de b?" |
a.Overlaps(b) | "Têm pelo menos um item em comum?" |
a.SetEquals(b) | "Têm exatamente os mesmos itens?" |
4. Maiúsculas e minúsculas
Para o HashSet<string>, "CSharp" e "csharp" são itens diferentes. Como no Dictionary, um comparador na criação muda isso:
var tags = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "CSharp", "csharp", "CSHARP", "dotnet" };
Console.WriteLine(tags.Count);
Console.WriteLine(tags.Contains("DotNet"));
$ dotnet run caixa.cs 2 « as três formas de "csharp" contaram como uma só True
5. Conjuntos de objetos seus: class × record
Para saber se um item "já está lá", o HashSet precisa comparar itens. Com números e textos, ele compara o conteúdo. Com objetos de uma class sua, o padrão é comparar a identidade: dois objetos criados com new são dois objetos diferentes, mesmo com os mesmos dados (veja Valor × Referência).
var pontosClasse = new HashSet<PontoClasse>();
pontosClasse.Add(new PontoClasse { X = 1, Y = 2 });
pontosClasse.Add(new PontoClasse { X = 1, Y = 2 });
Console.WriteLine($"class: {pontosClasse.Count}");
var pontosRecord = new HashSet<PontoRecord>();
pontosRecord.Add(new PontoRecord(1, 2));
pontosRecord.Add(new PontoRecord(1, 2));
Console.WriteLine($"record: {pontosRecord.Count}");
class PontoClasse
{
public int X { get; set; }
public int Y { get; set; }
}
record PontoRecord(int X, int Y);
$ dotnet run objetos.cs class: 2 « dois objetos diferentes, mesmo com X = 1 e Y = 2 record: 1 « o record compara os dados: é o mesmo ponto
Um record já vem com Equals e GetHashCode que comparam os dados, por isso o segundo ponto (1, 2) não entrou. Numa class, você teria de escrever esses dois métodos (aula A Classe object). Regra prática: para guardar objetos "de dados" num HashSet ou usá-los como chave de um Dictionary, prefira record.
6. Mudar o conjunto dentro do foreach
Como no Dictionary, você não pode acrescentar itens no conjunto que o foreach está percorrendo:
var album = new HashSet<int> { 7, 12, 30 };
foreach (int fig in album)
{
album.Add(fig + 100);
}
$ dotnet run foreach.cs Unhandled exception. System.InvalidOperationException: Collection was modified; enumeration operation may not execute. at System.Collections.Generic.HashSet`1.Enumerator.MoveNext() at Program.<Main>$(String[] args) in /home/ana/cs/foreach.cs:line 3 $ echo $? 134
"A coleção foi modificada; o percurso não pode continuar." A linha apontada é a 3, a do foreach: o Add funcionou, mas o foreach percebeu a mudança quando foi buscar o próximo item (MoveNext). Para acrescentar, junte os itens novos em outra coleção e acrescente depois do laço. E, para tirar todos os itens que cumprem uma condição, o HashSet tem um método pronto, o RemoveWhere ("remover onde"):
var album = new HashSet<int> { 7, 12, 30, 41, 50 };
int tirados = album.RemoveWhere(n => n % 2 == 0);
Console.WriteLine($"Tirados: {tirados}");
Console.WriteLine(string.Join(", ", album.Order()));
$ dotnet run removewhere.cs Tirados: 3 7, 41
O n => n % 2 == 0 é uma lambda: uma minifunção que recebe cada número n e diz se ele deve sair (aqui, os pares: resto 0 na divisão por 2). O RemoveWhere devolve quantos itens tirou.
Qual coleção usar?
| Você quer… | Use |
|---|---|
| Itens em ordem, com posição, repetidos permitidos | List<T> |
| Itens sem repetir; perguntar "tem?" rápido; a ordem não importa | HashSet<T> |
| Itens sem repetir, sempre em ordem | SortedSet<T> |
| Ligar cada item a um valor (nome → nota) | Dictionary<TKey, TValue> |
Quando dá errado
error CS0021: Não é possível aplicar a indexação com [] a uma expressão do tipo "HashSet<int>"
O que significa: você tentou pegar um item pela posição, como numa lista (album[0]). Um conjunto não tem posições. Em inglês: Cannot apply indexing with [] to an expression of type 'HashSet<int>'.
$ dotnet run indice.cs /home/ana/cs/indice.cs(2,19): error CS0021: Não é possível aplicar a indexação com [] a uma expressão do tipo "HashSet<int>" Ocorreu uma falha no build. Corrija os erros de build e execute novamente.
Como consertar: pergunte com Contains, percorra com foreach ou, se você precisa mesmo de posições, use uma List<T>.
InvalidOperationException: Collection was modified; enumeration operation may not execute.
O que significa: você acrescentou itens no conjunto enquanto um foreach o percorria.
Como consertar: guarde os itens novos em outra coleção e acrescente depois do laço (por exemplo, com UnionWith). Para tirar itens por uma condição, use RemoveWhere.
O que significa: não há mensagem de erro: o Count simplesmente sai maior do que você esperava. Os itens são de uma class sua, que compara identidade, e não os dados (seção 5).
Como consertar: transforme a classe num record, ou escreva Equals e GetHashCode nela.
O que significa: o programa contava com a ordem do HashSet, que não é garantida e muda depois de remoções.
Como consertar: use SortedSet<T> ou ordene ao mostrar (.Order()).
Referência rápida
| Escrita | O que faz |
|---|---|
new HashSet<T>() | Conjunto vazio |
new HashSet<T>(colecao) | Conjunto com os itens da coleção, sem repetidos |
new HashSet<string>(StringComparer.OrdinalIgnoreCase) | Textos sem diferenciar maiúsculas |
s.Add(x) | Acrescenta; true se entrou, false se já estava |
s.Contains(x) | Está no conjunto? (muito rápido) |
s.Remove(x) | Tira; true se estava lá |
s.RemoveWhere(x => …) | Tira todos os que cumprem a condição; devolve quantos |
s.Count / s.Clear() | Quantos itens / tira todos |
a.UnionWith(b) | a passa a ter também os itens de b |
a.IntersectWith(b) | a fica só com os itens que estão nos dois |
a.ExceptWith(b) | a perde os itens que estão em b |
a.SymmetricExceptWith(b) | a fica com o que está em um só dos dois |
a.IsSubsetOf(b), IsSupersetOf, Overlaps, SetEquals | Perguntas de conjunto; devolvem bool |
SortedSet<T> | Conjunto sempre ordenado |
Exercícios
1. Perguntar
Complete para perguntar se a figurinha 40 está no álbum:
2. Só o que falta
Complete para que faltam fique só com as figurinhas que não estão em tenho:
3. O conjunto s já tem o 7. O que s.Add(7) faz?
4. Com var a = new HashSet<int> { 1, 2, 3 }; e a.IntersectWith(new[] { 2, 3, 4 });, quanto vale a.Count?
5. Você guardou dois objetos new Aluno { Nome = "Ana" } (uma class sem Equals) num HashSet<Aluno>. Quanto vale o Count?
Missão final: troca de figurinhas
Missão: a Ana e o Bruno vão trocar figurinhas. Usando cópias dos conjuntos (para não estragar os originais), calcule com ExceptWith as figurinhas que o Bruno tem e a Ana não tem, e com IntersectWith as que os dois têm. Mostre cada resultado em ordem crescente com string.Join(", ", conjunto.Order()). No fim, mostre que a Ana continua com 5 figurinhas.
Bruno pode dar: 3, 8 Os dois têm: 2, 5, 9 A Ana continua com 5
var ana = new HashSet<int> { 1, 2, 4, 5, 9 };
var bruno = new HashSet<int> { 2, 3, 5, 8, 9 };
// 1. copie o conjunto do Bruno e tire o que a Ana já tem
// 2. copie o conjunto da Ana e fique só com o que o Bruno também tem
Console.WriteLine($"A Ana continua com {ana.Count}");
var ana = new HashSet<int> { 1, 2, 4, 5, 9 };
var bruno = new HashSet<int> { 2, 3, 5, 8, 9 };
var podeDar = new HashSet<int>(bruno);
podeDar.ExceptWith(ana);
Console.WriteLine($"Bruno pode dar: {string.Join(", ", podeDar.Order())}");
var emComum = new HashSet<int>(ana);
emComum.IntersectWith(bruno);
Console.WriteLine($"Os dois têm: {string.Join(", ", emComum.Order())}");
Console.WriteLine($"A Ana continua com {ana.Count}");
Resumo
- Um
HashSet<T>é um conjunto: sem repetidos, sem posições e com umContainsmuito rápido. Adddevolvetruese o item entrou efalsese ele já estava (sem exceção).Removedevolve se tirou.new HashSet<T>(lista)tira os repetidos de uma lista em uma linha.- A ordem não é garantida (muda depois de remoções). Para ordem, use
SortedSet<T>ou.Order()ao mostrar. UnionWith,IntersectWith,ExceptWitheSymmetricExceptWithmudam o conjunto: copie antes se precisar do original.- Objetos de uma
classsão comparados pela identidade; os de umrecord, pelos dados. conjunto[0]dáCS0021; acrescentar dentro doforeachdáInvalidOperationException.