What Is HashSet<T> in C#?
HashSet<T> is a generic collection in C# that stores
unique elements. Unlike a list, a hash set does not allow
the same value to appear more than once.
A hash set is designed primarily for fast membership checks and set-based operations such as union, intersection, and difference. It is useful when an application needs to determine whether a value already exists without maintaining elements by numeric index.
Why Use HashSet<T>?
Consider a collection of technology tags where duplicate values should not be stored.
var tags = new HashSet<string>();
tags.Add("C#");
tags.Add(".NET");
tags.Add("C#");
Console.WriteLine(tags.Count);
// 2
The second attempt to add "C#" does not create another
element. The hash set remains a collection of unique values.
Creating a HashSet in C#
HashSet<T> is available in the
System.Collections.Generic namespace.
using System.Collections.Generic;
HashSet<int> numbers = new HashSet<int>();
The type parameter T determines the type of values that the
hash set can contain.
Initializing a HashSet with Values
A hash set can be initialized with values using collection initializer syntax. Duplicate values in the initializer are automatically ignored.
var numbers = new HashSet<int>
{
10,
20,
30,
20,
10
};
Console.WriteLine(numbers.Count);
// 3
Adding Elements with Add()
The Add() method inserts an element if it is not already
present. It returns true when a new element is added and
false when the value already exists.
var languages = new HashSet<string>();
bool first = languages.Add("C#");
bool second = languages.Add("C#");
Console.WriteLine(first);
// True
Console.WriteLine(second);
// False
The return value makes Add() useful when the application needs
to know whether a value was newly inserted.
Checking for an Element with Contains()
The Contains() method checks whether a value exists in the
hash set. Hash-based membership checks are typically O(1) on
average.
var technologies = new HashSet<string>
{
"C#",
"ASP.NET Core",
"SQL"
};
Console.WriteLine(technologies.Contains("C#"));
// True
Console.WriteLine(technologies.Contains("Java"));
// False
Removing Elements
The Remove() method removes a specified value. It returns
true when the value existed and was removed.
var numbers = new HashSet<int>
{
10,
20,
30
};
bool removed = numbers.Remove(20);
Console.WriteLine(removed);
// True
Console.WriteLine(numbers.Count);
// 2
RemoveWhere()
RemoveWhere() removes every element that satisfies a supplied
condition. This is useful when multiple values need to be removed based
on a rule.
var numbers = new HashSet<int>
{
5,
10,
15,
20,
25
};
numbers.RemoveWhere(number => number % 2 == 0);
foreach (var number in numbers)
{
Console.WriteLine(number);
}
// 5
// 15
// 25
Clearing a HashSet
The Clear() method removes all elements from the hash set.
var tags = new HashSet<string>
{
"C#",
".NET",
"Azure"
};
tags.Clear();
Console.WriteLine(tags.Count);
// 0
Iterating Through a HashSet
A hash set can be traversed using foreach. Unlike a list,
elements are not accessed using numeric indexes.
var skills = new HashSet<string>
{
"C#",
"SQL",
"Azure"
};
foreach (var skill in skills)
{
Console.WriteLine(skill);
}
A HashSet<T> should be used for membership and set
semantics rather than when an application depends on numeric indexing or
a particular ordering.
HashSet Count
The Count property returns the number of unique elements
currently stored.
var users = new HashSet<int>
{
101,
102,
103,
102
};
Console.WriteLine(users.Count);
// 3
Union of Two HashSets
A union combines the elements from two sets. Duplicate values appear only once.
var frontend = new HashSet<string>
{
"HTML",
"CSS",
"JavaScript"
};
var backend = new HashSet<string>
{
"C#",
"SQL",
"JavaScript"
};
frontend.UnionWith(backend);
Console.WriteLine(string.Join(", ", frontend));
UnionWith() modifies the current hash set. If the original
sets need to remain unchanged, create a copy before performing the
operation.
Intersection of Two HashSets
An intersection keeps only the elements that exist in both sets.
var teamA = new HashSet<string>
{
"C#",
"SQL",
"Azure"
};
var teamB = new HashSet<string>
{
"SQL",
"Azure",
"Docker"
};
teamA.IntersectWith(teamB);
Console.WriteLine(string.Join(", ", teamA));
// SQL, Azure
Difference Between HashSets
ExceptWith() removes every element from the current set that
also exists in another set.
var allSkills = new HashSet<string>
{
"C#",
"SQL",
"Azure",
"Docker"
};
var knownSkills = new HashSet<string>
{
"C#",
"SQL"
};
allSkills.ExceptWith(knownSkills);
Console.WriteLine(string.Join(", ", allSkills));
// Azure, Docker
Symmetric Difference
SymmetricExceptWith() keeps elements that belong to one set
but not both sets. Values shared by both sets are removed.
var first = new HashSet<int>
{
1,
2,
3
};
var second = new HashSet<int>
{
3,
4,
5
};
first.SymmetricExceptWith(second);
Console.WriteLine(string.Join(", ", first));
// 1, 2, 4, 5
Checking Set Relationships
HashSet provides methods for determining whether one set is a subset, superset, or proper subset of another set.
var required = new HashSet<string>
{
"C#",
"SQL"
};
var available = new HashSet<string>
{
"C#",
"SQL",
"Azure",
"Docker"
};
Console.WriteLine(available.IsSupersetOf(required));
// True
Console.WriteLine(required.IsSubsetOf(available));
// True
Overlaps()
The Overlaps() method checks whether two sets have at least
one element in common.
var teamA = new HashSet<int>
{
1,
2,
3
};
var teamB = new HashSet<int>
{
5,
6,
3
};
Console.WriteLine(teamA.Overlaps(teamB));
// True
HashSet with Custom Objects
A hash set can store custom objects. For reference types, determining whether two objects are duplicates depends on equality semantics and the associated hashing behavior.
A record is convenient when value-based equality is desired because its generated equality members consider its values.
record User(int Id, string Name);
var users = new HashSet<User>();
users.Add(new User(1, "Asha"));
users.Add(new User(2, "Rahul"));
users.Add(new User(1, "Asha"));
Console.WriteLine(users.Count);
// 2
Equality and Hashing
Hash-based collections rely on equality and hash codes to determine
whether values are equivalent. When using custom classes, the equality
implementation should be consistent with GetHashCode().
Changing fields that participate in equality or hashing after an object has been inserted can make collection behavior difficult to reason about. Immutable keys or value-based types are often safer choices.
HashSet Time Complexity
HashSet operations are generally designed for constant-time average performance. Actual performance depends on hashing, equality comparisons, collisions, and resizing.
| Operation | Typical Complexity |
|---|---|
| Add | O(1) average |
| Contains | O(1) average |
| Remove | O(1) average |
| Iteration | O(n) |
| Clear | O(n) |
HashSet vs List<T>
Both collections can store multiple values, but they are optimized for different requirements.
| Feature | List<T> | HashSet<T> |
|---|---|---|
| Duplicates | Allowed | Not stored |
| Indexed access | Supported | Not supported |
| Contains | O(n) generally | O(1) average |
| Primary purpose | Ordered sequence | Uniqueness and membership |
HashSet vs Dictionary
A HashSet<T> stores unique values, while a
Dictionary<TKey, TValue> associates unique keys with
values.
| Requirement | Collection |
|---|---|
| Store unique values | HashSet<T> |
| Check whether a value exists | HashSet<T> |
| Map a key to a value | Dictionary<TKey, TValue> |
| Retrieve an object using an ID | Dictionary<TKey, TValue> |
Common HashSet Mistakes
- Expecting a hash set to provide numeric index access.
- Using a hash set when duplicate values are meaningful.
- Assuming iteration order should be used as application logic.
- Using mutable objects whose equality or hash code can change after insertion.
- Using a list when frequent membership checks and uniqueness are the primary requirements.
- Confusing a hash set with a dictionary and expecting key-value pairs.
Practical Example: Removing Duplicate Skills
A common use case for HashSet<T> is removing duplicate
values from a collection. Each value is inserted into the set, and only
unique values remain.
var skills = new[]
{
"C#",
"SQL",
"Azure",
"C#",
"Docker",
"SQL"
};
var uniqueSkills = new HashSet<string>(skills);
foreach (var skill in uniqueSkills)
{
Console.WriteLine(skill);
}
Console.WriteLine($"Unique skills: {uniqueSkills.Count}");
Key Takeaways
HashSet<T>stores unique elements.- Duplicate values are not added to the set.
Add()returns whether a new element was inserted.Contains()provides typically O(1) average membership checks.Remove()deletes an element by value.- HashSet supports union, intersection, difference, and other set operations.
- Hash sets do not provide numeric index-based access.
- Equality and hash-code behavior is important when storing custom objects.
- Use HashSet when uniqueness and membership checks are more important than ordering or indexing.