Back to Blog
C#

C# LINQ ToHashSet: Convert Query Results to a HashSet

c# linq tohashset: Learn how to use C# LINQ ToHashSet to convert query results into a HashSet, including behavior, performance, and common pitfalls.

C#LINQHashSetCollectionsEqualityComparer
Diagram showing LINQ query results being converted into a HashSet in C#

The ToHashSet() method in C# LINQ converts any IEnumerable<T> into a HashSet<T>. It is the set-based counterpart to ToList() and ToArray(), and it applies the default equality comparer unless you provide a custom one. This article explains how c# linq tohashset works, when it is the right choice, and where it can cause subtle problems.

The ToHashSet Method and Its Signature

ToHashSet() is an extension method defined in System.Linq.Enumerable. It is available for any IEnumerable<T> and returns a HashSet<T> containing the elements of the source sequence. The simplest call looks like this:

var numbers = new[] { 1, 2, 2, 3, 3, 3 }; HashSet<int> uniqueNumbers = numbers.ToHashSet();

Because HashSet<T> does not allow duplicate elements, the resulting set contains only 1, 2, and 3. The order of elements is not preserved; HashSet<T> does not guarantee any particular iteration order.

The method has an overload that accepts an IEqualityComparer<T>:

var words = new[] { "apple", "Apple", "banana" }; var caseInsensitiveSet = words.ToHashSet(StringComparer.OrdinalIgnoreCase);

This overload is useful when you need custom equality rules, such as case-insensitive string comparison or a custom class implementing IEquatable<T>.

How ToHashSet Differs from ToList and ToArray

The three conversion methods serve different purposes. ToList() returns a List<T>, ToArray() returns a T[], and ToHashSet() returns a HashSet<T>. The key difference is the data structure's contract:

MethodReturn TypeDuplicate HandlingLookup ComplexityOrder Preservation
ToList()`List<T>``Preserves duplicatesO(n) for index, O(n) for ContainsYes
ToArray()T[]Preserves duplicatesO(n) for index, O(n) for ContainsYes
ToHashSet()HashSet<T>Removes duplicatesO(1) average for ContainsNo

HashSet<T> is optimized for membership tests. If your primary operation is checking whether an element exists, ToHashSet() is more efficient than ToList() or ToArray() for large collections. However, if you need indexed access or preserve insertion order, a list or array is the better choice.

Using ToHashSet with Custom Equality Comparison

When the default equality comparer is not sufficient, pass a custom IEqualityComparer<T> to the overload. This is common when working with strings where case sensitivity matters, or with complex objects where you want to compare only a subset of properties.

public class Product { public int Id { get; set; } public string Name { get; set; } } var products = new List<Product> { new Product { Id = 1, Name = "Laptop" }, new Product { Id = 1, Name = "Laptop" }, new Product { Id = 2, Name = "Mouse" } }; var uniqueById = products.ToHashSet(new ProductIdComparer());

The comparer would implement IEqualityComparer<Product> and compare only the Id property. Without this, HashSet<Product> uses reference equality, so two objects with the same Id but different references would both be included.

When ToHashSet Is a Good Choice

Use ToHashSet() when you need a set of unique elements and plan to perform many Contains operations. For example, filtering a list against a set of allowed IDs is faster than repeatedly scanning a list.

var allowedIds = new[] { 10, 20, 30 }.ToHashSet(); var records = GetRecords(); // IEnumerable<Record> var filtered = records.Where(r => allowedIds.Contains(r.Id)).ToList();

The Contains call on the HashSet is O(1) on average, making the filter efficient even for large allowedIds.

It is also useful when you need to deduplicate a a sequence without caring about order. The Distinct() method also removes duplicates, but it returns an IEnumerable<T> and does not materialize the result. If you need a concrete set to pass to another method, ToHashSet() is the direct way.

Performance and Memory Considerations

Building a HashSet<T> requires hashing every element and handling collisions. This is generally more expensive than building a List<T> or array, which simply copies references. The tradeoff is that subsequent lookups are much faster.

For small collections, the overhead of hashing may outweigh the lookup benefit. If you are only iterating the collection once and never checking membership, ToList() or ToArray() is more appropriate.

Memory usage also differs. HashSet<T> uses a hash table with buckets, so it consumes more memory per element than a list or array. If memory is constrained and you do not need fast lookup, avoid ToHashSet().

There is no built-in way to specify the initial capacity of the HashSet when using ToHashSet(). If you know the approximate size, you can create a HashSet manually and add elements, but for most cases the default capacity is acceptable.

Common Pitfalls and Edge Cases

One subtle issue is that ToHashSet() silently removes duplicates. If your source sequence contains duplicates and you expect to keep them, this behavior will surprise you. Always check whether the deduplication is intentional.

Another pitfall is relying on order. HashSet<T> does not guarantee any specific iteration order, and the order can change between runs depending on the hash codes of the elements. If you need a stable order, sort the result after conversion or use a List<T>.

When using a custom comparer, ensure it is consistent with GetHashCode(). If two objects are equal according to the comparer, they must have have the same hash code; otherwise, the HashSet may not behave correctly.

ToHashSet() also throws an ArgumentNullException if the source sequence is null. This is the same behavior as other LINQ conversion methods.

ToHashSet with LINQ Queries and Deferred Execution

ToHashSet() is a greedy operator. It immediately enumerates the source sequence and builds the set. This is important when working with LINQ queries that have deferred execution. For example:

var query = numbers.Where(n => n > 2); var set = query.ToHashSet();

The Where clause is evaluated when ToHashSet() is called, not before. If the underlying data changes after the query is defined but before ToHashSet() is invoked, the result reflects the current data.

If you need to reuse the query result multiple times without re-executing the source, materializing with ToHashSet() is a good way to capture a snapshot. However, be aware that the source is fully enumerated, which may have side effects if it is a generator or a database query.

Alternative Approaches and When to Avoid ToHashSet

If you only need to remove duplicates and do not need a concrete collection, Distinct() is lighter because it defers execution. If you need indexed access or order, use ToList() or ToArray(). If you need a dictionary keyed by a property, ToDictionary() may be more appropriate.

ToHashSet() is not suitable when you need to store elements in a sorted order; use SortedSet<T> instead, but note that SortedSet<T> does not have a direct LINQ conversion method. You would need to construct it manually.

In summary, ToHashSet() is a targeted tool for creating a set with fast membership checks. Understanding its behavior—deduplication, orderlessness, and equality rules—helps you decide when to use it and when another collection type is a better fit.

c# linq tohashset: Practical Usage and Code Examples | RYUSLOG DEV