Back to Blog
C#

C# List vs HashSet: Choosing the Right Collection

c# list vs hashset: Compare C# List and HashSet for ordering, duplicates, and lookup performance. Learn which collection fits your scenario with code examples.

C# CollectionsHashSetListPerformance.NET
Side-by-side comparison of a List and a HashSet in C#, showing ordered versus unordered storage and fast lookup

When you need to store a set of objects in C#, List<T> and HashSet<T> are two of the most common choices. They look similar at first glance, but they solve different problems. The decision between c# list vs hashset comes down to how you intend to access the data and what guarantees you need about order and uniqueness.

What List<T> Provides

List<T> is an ordered, index-based collection. It preserves the order in which elements are added, allows duplicate values, and provides random access via an index. This makes it the natural choice when you need to iterate in insertion order or access elements by position.

var fruits = new List<string> { "apple", "banana", "apple" }; Console.WriteLine(fruits[0]); // apple Console.WriteLine(fruits.Count); // 3

The list stores elements in a contiguous array internally. Adding to the end is usually O(1), but inserting in the middle requires shifting elements. Finding an element by value requires a linear scan, which is O(n) in the worst case.

What HashSet<T> Provides

HashSet<T> is an unordered collection that guarantees uniqueness. It uses a hash table internally, so adding, removing, and checking for membership are all O(1) on average. However, it does not preserve insertion order, and it does not allow duplicate elements.

var uniqueFruits = new HashSet<string> { "apple", "banana", "apple" }; Console.WriteLine(uniqueFruits.Count); // 2 Console.WriteLine(uniqueFruits.Contains("banana")); // True

If you try to add a duplicate, the Add method returns false and the set remains unchanged. This makes HashSet<T> ideal for deduplication and fast membership tests.

Lookup Performance: The Core Difference

The most significant difference between the two collections is lookup speed. List<T> uses linear search, so Contains becomes slower as the collection grows. HashSet<T> uses the hash code of each element to compute a bucket, so Contains is nearly constant time regardless of size.

OperationList<T>HashSet<T>
AddO(1) endO(1) average
ContainsO(n)O(1) average
RemoveO(n)O(1) average
Access by indexO(1)Not supported
Order preservedYesNo
Duplicates allowedYesNo

For a small collection (say, fewer than 20 elements), the difference is negligible. But when you are checking membership against thousands of items, HashSet<T> will outperform List<T> by a wide margin. The hash table avoids scanning the entire collection.

Memory and Allocation Considerations

HashSet<T> has a higher memory footprint than List<T> because it stores hash buckets and uses an internal array that grows as needed. Each element also has a hash code computed, which may require additional computation for complex types. If you are storing a small number of items and need to iterate frequently, List<T> is more memory-efficient.

However, if you are deduplicating a large stream of data, the memory saved by not storing duplicates often outweighs the overhead of the hash table. The tradeoff is between memory and lookup speed.

When to Use List<T>

Use List<T> when:

  • You need to preserve the order of elements.
  • You need to access elements by index.
  • Duplicates are allowed or even expected.
  • The collection size is small and lookup performance is not critical.

For example, a list of items to display in a UI, where the user expects a specific order and duplicates are possible, is a natural fit for List<T>.

When to Use HashSet<T>

Use HashSet<T> when:

  • You need to ensure elements are unique.
  • You frequently check whether an element exists.
  • Order does not matter.
  • You are working with a large collection where O(1) lookup matters.

A common use case is filtering out duplicate records from a log file or validating that a set of IDs is unique before processing.

Custom Equality and Hash Codes

Both collections use the default equality comparer unless you specify one. For HashSet<T>, the correctness of the set depends on consistent GetHashCode and Equals implementations. If you store custom objects, you must override these methods or provide an IEqualityComparer<T>.

public class Product { public int Id { get; set; } public string Name { get; set; } public override bool Equals(object obj) => obj is Product p && Id == p.Id; public override int GetHashCode() => Id.GetHashCode(); }

If you fail to implement GetHashCode correctly, the set may allow duplicates or fail to find existing items. This is a common source of bugs when switching from List<T> to HashSet<T>.

Combining Both: When Order and Uniqueness Matter

Sometimes you need both order and uniqueness. HashSet<T> does not preserve order, and List<T> does not enforce uniqueness. In that case, you can maintain a List<T> for order and a HashSet<T> for fast duplicate checks. This pattern is useful when processing a stream of items that must appear in a specific order but only once.

var order = new List<string>(); var seen = new HashSet<string>(); void AddUnique(string item) { if (seen.Add(item)) { order.Add(item); } }

This gives you O(1) duplicate detection and preserves insertion order, at the cost of maintaining two collections.

Common Pitfalls and Edge Cases

One subtle issue is that HashSet<T> does not guarantee iteration order. The order can change when the set is modified, so never rely on it. Another is that List<T> allows null as a valid element, while HashSet<T> will throw an ArgumentNullException when adding null if T is a reference type and the default comparer is used. Always check for null before adding to a HashSet<T>.

Also, be aware that HashSet<T> is not thread-safe. If you need concurrent access, use ConcurrentDictionary<T, byte> or synchronize access manually. List<T> has the same limitation, so neither is safe for multi-threaded writes without external locking.

c# list vs hashset: Practical Usage and Code Examples | RYUSLOG DEV