Menu

HashSet em C#: itens únicos, Contains e operações de conjunto

HashSet<T> guarda itens únicos e responde ao Contains em tempo constante. Veja como o Add informa duplicatas, como remover duplicados de uma lista, como fazer união, interseção e diferença de conjuntos e como fazer um conjunto comparar seus próprios objetos pelo valor.

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

Um HashSet<T> é uma coleção em que cada item aparece no máximo uma vez. Ele não tem índice nem ordem garantida, e em troca verifica se um item pertence ao conjunto em tempo praticamente constante: achar uma tag entre um milhão leva mais ou menos o mesmo tempo que achá-la entre dez.

Adicionando itens: Add retorna bool

Saída:

True
False
3
True
True
False
2

Adicionar uma duplicata não é um erro; Add simplesmente retorna false e o conjunto não muda. Esse valor de retorno é o que o método tem de mais útil. Ele junta "já vi isto?" e "guarde isto" em uma única chamada:

Saída:

Duplicate: ana@x.com
Duplicate: ben@x.com
3 unique

Por que o Contains é rápido

List<T>.Contains compara o valor com cada elemento, um de cada vez, então o custo cresce com a lista. Um HashSet<T> calcula o hash code do item, pula para o bucket desse código e compara só os poucos itens guardados ali. Para um teste de pertinência dentro de um laço, isso transforma um passo O(n) em um passo O(1), e um laço aninhado sobre duas listas em uma única passada:

// Slow on large inputs: Contains scans bannedList for every order.
var flagged = orders.Where(o => bannedList.Contains(o.CustomerId));

// Fast: build the set once, then each lookup is constant time.
var banned = new HashSet<int>(bannedList);
var flagged2 = orders.Where(o => banned.Contains(o.CustomerId));

Montar o conjunto custa uma passada pela lista, então só compensa quando você faz mais do que algumas consultas.

Removendo duplicados de uma lista

Há três formas comuns, e elas diferem no que acontece com a ordem:

Saída:

Lima, Oslo, Pune, Kyiv
4
Lima, Oslo, Pune, Kyiv

Distinct é o padrão certo quando você quer uma lista de volta. A versão que altera a própria lista funciona porque RemoveAll chama o predicado uma vez por elemento, em ordem: seen.Add retorna false para a segunda cópia em diante, então exatamente essas são removidas.

Operações de conjunto: união, interseção, diferença

HashSet<T> tem as operações da teoria dos conjuntos. Os métodos ...With mudam o conjunto em que são chamados e não retornam nada.

Saída:

Union:     Ana, Ben, Chloe, Dev
Intersect: Ben, Chloe
Except:    Ana
Symmetric: Ana, Dev
True
True
False
True

O argumento pode ser qualquer IEnumerable<T>: um array, uma lista ou outro conjunto. SetEquals ignora a ordem e as duplicatas do argumento. O auxiliar Show ordena antes de imprimir porque a ordem de enumeração de um conjunto não é algo em que se possa confiar.

O LINQ tem métodos equivalentes que retornam uma nova sequência e não mexem nas entradas: monday.Union(tuesday), monday.Intersect(tuesday), monday.Except(tuesday). Use-os quando você não quer alterar um conjunto, ou quando as entradas são listas.

Igualdade própria para suas classes

Um conjunto decide o que é "o mesmo item" com GetHashCode e Equals. Para uma classe que não os sobrescreve, os dois se baseiam na identidade do objeto, então dois objetos com campos iguais são dois itens diferentes:

Saída:

2
1
True

A regra: objetos que são Equals precisam retornar o mesmo GetHashCode. Sobrescreva só o Equals e o conjunto procura no bucket errado e continua aceitando duplicatas. No .NET Core 2.1 em diante, HashCode.Combine(X, Y) gera um bom hash code sem a aritmética escrita à mão.

Quando você não pode mudar a classe, ou precisa de outra noção de "o mesmo" para um conjunto específico, passe um IEqualityComparer<T> ao construtor. Strings já vêm com comparadores prontos:

Saída:

True
False
2

No C# 9 em diante, um record gera Equals e GetHashCode baseados em valor para você, então record Point(int X, int Y); funciona em um conjunto sem código extra.

Nunca mude um campo usado no GetHashCode enquanto o objeto estiver em um conjunto. O objeto continua no bucket do hash antigo, então Contains e Remove deixam de encontrá-lo.

Ordem e SortedSet

Um HashSet<T> é enumerado em uma ordem que você deve tratar como arbitrária. Se você precisa dos itens ordenados, ordene ao imprimir (set.OrderBy(x => x)) ou use SortedSet<T>, que mantém os itens sempre ordenados e adiciona Min, Max e consultas por intervalo a O(log n) por operação:

var ranks = new SortedSet<int> { 30, 10, 20 };
Console.WriteLine(string.Join(", ", ranks)); // 10, 20, 30
Console.WriteLine(ranks.Min);                // 10

HashSet vs List vs Dictionary

NecessidadeUse
Itens únicos, "está aqui?" rápidoHashSet<T>
Itens únicos, sempre ordenadosSortedSet<T>
Ordem, duplicatas, acesso por índiceList<T>
Um valor guardado sob cada chaveDictionary<TKey, TValue>

Um conjunto é um dicionário com chaves e sem valores. Se você se pegar escrevendo Dictionary<string, bool> só para registrar pertinência, um HashSet<string> diz a mesma coisa com mais clareza.

Erros comuns

  • Esperar uma ordem. Um conjunto não tem uma ordem confiável; ordene, ou use SortedSet<T>.
  • Classes próprias sem Equals e GetHashCode. Objetos que parecem iguais viram itens separados.
  • Sobrescrever só o Equals. Sempre sobrescreva o GetHashCode junto.
  • Alterar um item depois de adicioná-lo. O conjunto não consegue mais encontrá-lo.
  • Acessar um conjunto por índice. set[0] não compila; não existe índice. Converta com ToList() se precisar de posições.

Perguntas frequentes

O que é um HashSet em C#?

HashSet<T> é uma coleção de itens únicos sem ordem definida. Adicionar um item que já está presente não faz nada, e Contains responde em tempo praticamente constante, não importa o tamanho do conjunto, porque os itens são guardados pelo hash code, como as chaves de um Dictionary.

O que o HashSet.Add retorna?

Add retorna true quando o item foi adicionado e false quando ele já estava no conjunto. Isso faz de if (!seen.Add(x)) uma verificação de duplicata em uma linha: adiciona itens novos e avisa sobre os repetidos na mesma chamada.

Como remover duplicados de uma List em C#?

list.Distinct().ToList() retorna uma nova lista sem duplicados e mantém a primeira ocorrência de cada item na ordem original. new HashSet<T>(list) também remove duplicados, mas um conjunto não tem ordem garantida. Para remover duplicados na própria lista, use var seen = new HashSet<T>(); list.RemoveAll(x => !seen.Add(x));.

Quando usar um HashSet em vez de uma List?

Use um HashSet<T> quando você mais pergunta "este item está na coleção?" ou precisa que os itens sejam únicos. List<T>.Contains varre todos os elementos, então fica mais lento à medida que a lista cresce, e HashSet<T>.Contains não. Use uma List<T> quando ordem, duplicatas ou acesso por índice importam.

Por que meu HashSet contém objetos duplicados?

Sua classe não sobrescreve Equals e GetHashCode, então o conjunto compara referências, e dois objetos com os mesmos valores nos campos contam como diferentes. Sobrescreva os dois métodos (sempre juntos), ou passe um IEqualityComparer<T> ao construtor do conjunto.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR