HashSet in C#

Learn HashSet in C# with practical examples, explanations, best practices, and real-world coding techniques.

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.

Code Examples

using System;
using System.Collections.Generic;

var skills = new HashSet<string>();

skills.Add("C#");
skills.Add("SQL");
skills.Add("C#");

Console.WriteLine(skills.Count);
// 2

Console.WriteLine(skills.Contains("C#"));
// True
    
using System;
using System.Collections.Generic;

var numbers = new[]
{
    10,
    20,
    10,
    30,
    20,
    40
};

var uniqueNumbers = new HashSet<int>(numbers);

Console.WriteLine(string.Join(", ", uniqueNumbers));
// 10, 20, 30, 40
    
using System;
using System.Collections.Generic;

var backend = new HashSet<string>
{
    "C#",
    "SQL",
    "Azure"
};

var cloud = new HashSet<string>
{
    "Azure",
    "Docker",
    "Kubernetes"
};

var common = new HashSet<string>(backend);
common.IntersectWith(cloud);

var combined = new HashSet<string>(backend);
combined.UnionWith(cloud);

Console.WriteLine("Common:");
Console.WriteLine(string.Join(", ", common));

Console.WriteLine("Combined:");
Console.WriteLine(string.Join(", ", combined));
    

Pro Tip

Use HashSet<T> when your main requirements are uniqueness and fast membership checks. If you need to retrieve a value using a key, use Dictionary<TKey, TValue>; if you need ordered data and indexed access, use List<T>.

🎨 IgKnight Docs

Master HTML, CSS, JS, Python, .NET and more with easy-to-follow lessons and practical code examples.

© 2025 IgKnight Learning Platform. All rights reserved.