1. Introduction
Imagine this: you're building a system for an online store, and you need to keep a list of all unique product codes that have ever been sold. Or, even better, you're writing a social network app and need to quickly check if a username already exists. Or maybe you want to count how many unique words are in a big text?
In all these scenarios, we need a collection where each element appears only once. And that's where HashSet<T> comes in.
HashSet<T> is a collection that stores an unordered set of unique elements. The key word here is unique. If you try to add an element that's already in the HashSet, it'll just ignore your attempt and won't add a duplicate. It's like a club "for unique people only": if you're already inside, they won't let you in again.
Key features of HashSet<T>:
- Uniqueness: Guarantees that every element in the collection is present only once.
- Performance: Super fast at checking if an element exists, adding, and removing it. On average, these operations are done in constant time (O(1)), no matter how many elements you have! This is thanks to a mechanism called hashing.
- No order: Unlike List<T>, elements in HashSet<T> aren't stored in any particular order. You can't get an element by index (like, "the fifth element").
- Based on a hash table: Inside, HashSet<T> uses a hash table to store elements, which is what gives it such high performance. We're not gonna dive deep into how hash tables work right now (that's a topic for a more advanced lecture), but just imagine that every element "turns into" a special numeric code (a hash), which lets you find it super fast later.
Let's compare this to using a List<T> to store unique elements: you'd have to go through the whole list every time to make sure the element isn't there before adding it. That would be super slow for big lists. HashSet<T> does it instantly!
2. Why does a programmer need HashSet<T>?
The secret of unique collections
In programming, it's super common to need to store elements without duplicates. For example, you're parsing a list of user email addresses and want to make sure there are no duplicates. Or you're collecting unique file names read from a folder. The simplest solution is a collection where you can't add the same thing twice.
Sure, you could try to solve this with a List<T>, manually checking if the element is in the list before adding:
var users = new List<string>();
if (!users.Contains("vasya@example.com"))
users.Add("vasya@example.com");
But this solution sucks for big data — the Contains check on a List means looking through all the elements, and if you have thousands of users, your program will be as slow as an old Windows XP computer.
What does HashSet<T> do
HashSet<T>, on the other hand, guarantees that every element is stored only once. It's built on a hash table (just like a dictionary), so adding, searching, and removing are super fast — usually constant time, no need to look through everything.
3. Basics of working with HashSet<T>
Declaration and creation
To get started, you don't need to import any extra libraries — the class is already in the System.Collections.Generic namespace.
using System.Collections.Generic;
var emails = new HashSet<string>();
You can fill the collection with initial values right away by passing them to the constructor:
var fruits = new HashSet<string> { "apple", "banana", "pear", "banana" };
// "banana" appears twice, but will only be stored once!
Adding elements
Add elements with the Add method. If the element wasn't there yet, the method returns true. If it's already there — nothing happens, and the method returns false.
bool added = emails.Add("vasya@example.com"); // true, element added
added = emails.Add("vasya@example.com"); // false, already exists, not added
Fun fact: You can call Add a hundred times with the same value — HashSet won't get mad, it'll just politely ignore the repeats.
Checking for existence: Contains
To check if an element is stored, use the Contains method:
if (emails.Contains("vasya@example.com"))
Console.WriteLine("That email already exists!");
Removing elements
Removing is fast too:
emails.Remove("vasya@example.com");
If the element wasn't there — no big deal, the method just returns false.
4. Practical example
Let's make our student CRM, which we've been building throughout the course, a bit more advanced.
Requirement
Let's say, according to our system's rules, every user must have a unique username (login). Before adding a new user, we need to check for uniqueness, and if not — let them know.
Code example
using System;
using System.Collections.Generic;
class Program
{
static void Main()
{
// Collection for storing unique logins
var userNames = new HashSet<string>();
while (true)
{
Console.Write("Enter username (or leave to exit): ");
string name = Console.ReadLine();
if (name == "leave")
break;
if (userNames.Add(name))
{
Console.WriteLine("Name added successfully!");
}
else
{
Console.WriteLine("Error: this name is already taken, try another one.");
}
}
Console.WriteLine("User list:");
foreach (var user in userNames)
Console.WriteLine($"- {user}");
// Attention! The output order may be random.
}
}
That's how easy it is to ensure uniqueness. No need to manually check for existence — HashSet does it all for you.
5. How does HashSet<T> work inside? Why do we need a hash code
Analogy: storage cells
Imagine you have a huge stack of cards with logins, and a table with cells numbered from 0 to 1000. Each login you put in the cell whose number is calculated by a function (GetHashCode). If cards match — they end up in the same cell, and you quickly find out the login already exists.
The GetHashCode function
HashSet<T> doesn't just compare elements by value, it first calculates their hash code using the GetHashCode() method. For most built-in types (int, string, double, etc.) this is already optimized.
Fun fact: If you create your own class types and want to store them in a HashSet<T>, make sure to implement proper methods for equality comparison and getting a unique code (Equals and GetHashCode methods), so uniqueness works right. But more on that in future lectures.
Some typical mistakes when working with HashSet<T>
When people are just learning to use a unique value collection, a common trap is thinking that HashSet<T> stores elements in the order you added them. Nope! Hash sets don't guarantee any order, everything can end up in a random sequence. If you care about order — you need a different collection type, like SortedSet<T>, but that's another story.
Another popular mistake — trying to use an index:
string name = userNames[0]; // Error! HashSet<T> has no indexes.
Unlike an array or list, you can't access an element by number here. You can only loop through elements with foreach.
A third common confusion happens when serializing or saving a hash set to a file — because of the undefined order, elements may end up in a different sequence between program runs.
6. Set operations: union, intersection, difference
HashSet<T> offers a whole bunch of methods that make working with it feel like doing math set operations. Stuff like: union, intersection, difference, and symmetric difference.
Here are the main ones:
| Method | What it does |
|---|---|
|
Adds all elements from other to the hash set. |
|
Keeps only elements that are both here and in other. |
|
Removes from the current set the elements from other. |
|
Keeps only those elements that are either here or in other, but not in both. |
Example: intersection and union
Let's see an example. Suppose we have two sets of names:
var groupA = new HashSet<string> { "Anya", "Boris", "Vera" };
var groupB = new HashSet<string> { "Vera", "Gleb", "Dasha" };
// Find who is in both groups
var common = new HashSet<string>(groupA); // copy contents, otherwise groupA will change!
common.IntersectWith(groupB);
Console.WriteLine("In both groups:");
foreach (var name in common)
Console.WriteLine(name); // Will print "Vera"
// Combine all students from both groups so no one gets lost:
var all = new HashSet<string>(groupA);
all.UnionWith(groupB);
Console.WriteLine("All students:");
foreach (var name in all)
Console.WriteLine(name); // "Anya", "Boris", "Vera", "Gleb", "Dasha"
7. Extra methods and properties
Count — find out how many elements are in the set:
Console.WriteLine(userNames.Count);
Clear — delete everything (like CTRL+A, DELETE in real life):
userNames.Clear();
SetEquals, IsSubsetOf, IsSupersetOf — checks if sets are equal, if one is inside another, and so on. This helps if you're playing (or coding) something like "mathematician — who's cooler".
if (groupA.IsSubsetOf(groupB))
Console.WriteLine("Everyone from group A is in group B");
8. Storing your own objects in HashSet<T>
Like we mentioned above, standard types already know how to calculate hashes and compare for equality correctly.
But if we decide to store, say, users as objects, we'll need to make sure comparison is done by some property (like login):
class User
{
public string Login { get; set; }
public override bool Equals(object obj)
{
if (obj is User other)
return Login == other.Login;
return false;
}
public override int GetHashCode()
{
return Login.GetHashCode();
}
}
// Now you can:
var users = new HashSet<User>();
users.Add(new User { Login = "vasya" });
users.Add(new User { Login = "petya" });
users.Add(new User { Login = "vasya" }); // Won't be added!
Without overriding the Equals and GetHashCode methods, HashSet will treat all instances as different (even if the login matches), because by default it compares memory addresses.
GO TO FULL VERSION