When to Use HashSet vs List in C#

HashSet<T> is an unordered collection optimized for fast lookups, uniqueness enforcement, and set operations. List<T> is an ordered collection that allows duplicates and provides index-based access.ess.

Use HashSet<T> when you need to check for membership frequently (Contains), enforce uniqueness, or perform set operations like union/intersection. Use List<T> when order matters, you need indexed access, or duplicates are allowed.

HashSet has O(1) Contains/Add/Remove operations, while List has O(n) for Contains and Remove. List provides O(1) indexed access, which HashSet doesn't support.

Setup and Basic Comparison

C# Example Code
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

// List - ordered, allows duplicates
Console.WriteLine("=== List<T> - Ordered with duplicates ===");
List<string> namesList = new List<string> { "Alice", "Bob", "Alice", "Charlie" };
namesList.Add("Bob");  // Duplicates allowed

Console.WriteLine($"List count: {namesList.Count}");
Console.WriteLine("List contents:");
foreach (var name in namesList)
{
    Console.WriteLine($"  {name}");
}

// HashSet - unordered, unique items only
Console.WriteLine("\n=== HashSet<T> - Unique items only ===");
HashSet<string> namesSet = new HashSet<string> { "Alice", "Bob", "Alice", "Charlie" };
namesSet.Add("Bob");  // Duplicate not added

Console.WriteLine($"HashSet count: {namesSet.Count}");
Console.WriteLine("HashSet contents:");
foreach (var name in namesSet)
{
    Console.WriteLine($"  {name}");
}

Membership Testing and Index Access

C# Example Code
// Fast membership testing with HashSet
Console.WriteLine("\n=== Membership testing ===");
Console.WriteLine($"List contains 'Alice': {namesList.Contains("Alice")}");
Console.WriteLine($"HashSet contains 'Alice': {namesSet.Contains("Alice")}");

// Index access - List only
Console.WriteLine("\n=== Index access (List only) ===");
Console.WriteLine($"First item in list: {namesList[0]}");
Console.WriteLine($"Last item in list: {namesList[namesList.Count - 1]}");
// namesSet[0];  // Error: HashSet doesn't support indexing

Set Operations with HashSet

C# Example Code
// Set operations - HashSet speciality
Console.WriteLine("\n=== Set operations ===");
HashSet<int> setA = new HashSet<int> { 1, 2, 3, 4, 5 };
HashSet<int> setB = new HashSet<int> { 4, 5, 6, 7, 8 };

// Union - all unique items from both sets
HashSet<int> union = new HashSet<int>(setA);
union.UnionWith(setB);
Console.WriteLine($"Union: {string.Join(", ", union)}");

// Intersection - common items
HashSet<int> intersection = new HashSet<int>(setA);
intersection.IntersectWith(setB);
Console.WriteLine($"Intersection: {string.Join(", ", intersection)}");

// Difference - items in A but not in B
HashSet<int> difference = new HashSet<int>(setA);
difference.ExceptWith(setB);
Console.WriteLine($"Difference (A - B): {string.Join(", ", difference)}");

// Symmetric difference - items in either A or B but not both
HashSet<int> symmetricDiff = new HashSet<int>(setA);
symmetricDiff.SymmetricExceptWith(setB);
Console.WriteLine($"Symmetric Difference: {string.Join(", ", symmetricDiff)}");

Removing Duplicates

C# Example Code
// Removing duplicates from List using HashSet
Console.WriteLine("\n=== Removing duplicates ===");
List<int> numbersWithDupes = new List<int> { 1, 2, 2, 3, 3, 3, 4, 4, 5 };
HashSet<int> uniqueNumbers = new HashSet<int>(numbersWithDupes);
Console.WriteLine($"Original list: {string.Join(", ", numbersWithDupes)}");
Console.WriteLine($"Unique items: {string.Join(", ", uniqueNumbers)}");

Performance Comparison

C# Example Code
// Performance comparison
Console.WriteLine("\n=== Performance comparison ===");
PerformanceTest();

static void PerformanceTest()
{
    const int size = 10000;
    const int lookups = 1000;

    // Populate collections
    List<int> list = new List<int>();
    HashSet<int> set = new HashSet<int>();
    
    for (int i = 0; i < size; i++)
    {
        list.Add(i);
        set.Add(i);
    }

    Stopwatch sw = new Stopwatch();

    // List Contains - O(n)
    sw.Start();
    for (int i = 0; i < lookups; i++)
    {
        bool exists = list.Contains(size / 2);
    }
    sw.Stop();
    long listTime = sw.ElapsedMilliseconds;

    // HashSet Contains - O(1)
    sw.Restart();
    for (int i = 0; i < lookups; i++)
    {
        bool exists = set.Contains(size / 2);
    }
    sw.Stop();
    long setTime = sw.ElapsedMilliseconds;

    Console.WriteLine($"List.Contains: {listTime}ms");
    Console.WriteLine($"HashSet.Contains: {setTime}ms");
    Console.WriteLine($"HashSet is ~{(listTime > 0 ? listTime / Math.Max(setTime, 1) : 0)}x faster for lookups");
}

When to Use Each

Use List<T> when:

  • Order matters
  • Need indexed access
  • Duplicates are allowed/needed
  • Need to sort the collection

Use HashSet<T> when:

  • Need to check membership frequently
  • Must enforce uniqueness
  • Performing set operations
  • Order doesn't matter