C# LINQ Intersect: Set Intersection in Practice
c# linq intersect: Learn how to use C# LINQ Intersect to find common elements between collections, including custom equality comparers, performance considerations, and...
c# linq intersect requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
The Intersect method in LINQ returns the set intersection of two sequences: the elements that appear in both. It is part of the System.Linq namespace and works with any IEnumerable<T>. The most common usage is straightforward, but the behavior around equality, reference types, and performance can surprise developers who assume it works like a simple loop.
Using Intersect with Default Equality
For value types like int, string, or DateTime, Intersect uses the default equality comparer for that type. Here is the minimal example:
using System; using System.Collections.Generic; using System.Linq; var first = new[] { 1, 2, 3, 4 }; var second = new[] { 3, 4, 5, 6 }; var common = first.Intersect(second); foreach (var number in common) { Console.WriteLine(number); } // Output: 3, 4
The result is a new IEnumerable<int> that contains distinct elements that appear in both sequences. Duplicates in either source are ignored; Intersect returns a set, not a multiset. If first had { 1, 2, 2, 3 } and second had { 2, 2, 3 }, the result would still be { 2, 3 }.
This method uses deferred execution. The enumeration of the source sequences does not happen until you iterate over the result. If you need the result as a list or array, call ToList() or ToArray().
Custom Equality with IEqualityComparer
The default equality comparer for a type uses EqualityComparer<T>.Default, which relies on object.Equals and GetHashCode. For reference types, this means reference equality unless the type overrides those methods. When you need to compare objects based on specific properties, you can pass an IEqualityComparer<T> to the Intersect overload.
Consider a Product class with Id and Name properties. You want to find products that appear in both a warehouse list and a sales list, but you only care about the Id:
public class Product { public int Id { get; set; } public string Name { get; set; } } public class ProductIdComparer : IEqualityComparer<Product> { public bool Equals(Product x, Product y) { return x.Id == y.Id; } public int GetHashCode(Product obj) { return obj.Id.GetHashCode(); } } var warehouse = new[] { new Product { Id = 1, Name = "Laptop" }, new Product { Id = 2, Name = "Mouse" } }; var sales = new[] { new Product { Id = 2, Name = "Mouse" }, new Product { Id = 3, Name = "Keyboard" } }; var commonProducts = warehouse.Intersect(sales, new ProductIdComparer()); foreach (var product in commonProducts) { Console.WriteLine(product.Name); } // Output: Mouse
The comparer must implement both Equals and GetHashCode consistently. If two objects compare as equal, they must produce the same hash code. Otherwise, Intersect may produce incorrect results or throw exceptions in certain hash-based internal operations.
Intersect with Complex Types and Records
For custom classes that do not override Equals and GetHashCode, Intersect uses reference equality. Two objects with identical property values but different references will not be considered equal. This is a common source of confusion.
If you are using C# 9 or later, records provide value-based equality by default. The following works without a custom comparer:
public record Product(int Id, string Name); var warehouse = new[] { new Product(1, "Laptop"), new Product(2, "Mouse") }; var sales = new[] { new Product(2, "Mouse"), new Product(3, "Keyboard") }; var common = warehouse.Intersect(sales); foreach (var product in common) { Console.WriteLine(product.Name); } // Output: Mouse
Records generate Equals and GetHashCode based on all properties. If you need to compare only a subset of properties, a custom comparer is still necessary.
Performance Considerations
The internal implementation of Intersect is not publicly documented, but it is known to use a HashSet<T> internally to store elements from the first sequence and then iterate the second sequence, checking membership. This gives an average time complexity of O O(n + m), where n and m are the lengths of the sequences, assuming good hash distribution.
This is significantly faster than a naive nested loop, which would be O(n * m). However, the hash-based approach introduces overhead for hashing and memory allocation. For very small collections (fewer than a few dozen elements), a simple nested loop might be faster in practice, but the difference is negligible in most applications.
One important detail: Intersect enumerates the first sequence completely before it starts yielding results. It builds a hash set of the first sequence's distinct elements. The second sequence is then enumerated lazily, and each element is checked against the set. This means the first sequence is fully buffered in memory, while the second sequence is streamed. If you have a huge first sequence and a small second sequence, this can be memory-intensive. In such cases, you might consider reversing the order of the sequences, but that only helps if the first sequence is the one you want to buffer. There is no way to avoid buffering one side entirely because the method must know all distinct elements of one set to perform the intersection.
For sequences that are already sets (e.g., HashSet<T>), you can use the IntersectWith method on HashSet<T> directly, which modifies the set in place and can be more efficient if you do not need the original set afterward. LINQ's Intersect always returns a new sequence and does not modify the source.
Intersect vs. Other Set Operations
Intersect is one of three set operations in LINQ, along with Union and Except. Understanding their differences helps you choose the right one.
| Operation | Returns | |
|---|--|
| Intersect | Elements that appear in both sequences |
| Union | Distinct elements from both sequences |
| Except | Elements from the first sequence that do not appear in the second |
All three use the same equality semantics and have overloads that accept an IEqualityComparer<T>. They also all return distinct elements. For example:
var first = new[] { 1, 2, 3, 4 }; var second = new[] { 3, 4, 5, 6 }; var union = first.Union(second); // { 1, 2, 3, 4, 5, 6 } var except = first.Except(second); // { 1, 2 } var intersect = first.Intersect(second); // { 3, 4 }
When you need to find elements that are in both sequences, Intersect is the direct tool. If you need to combine sequences without duplicates, Union is appropriate. If you need to subtract one set from another, Except is the choice.
Common Pitfalls and Edge Cases
One common mistake is expecting Intersect to preserve duplicate occurrences. It does not; it returns distinct elements. If you need a multiset intersection (i.e., each element appears as many times as it appears in both sequences), you must implement that logic manually. For example, using a Dictionary to count occurrences.
Another pitfall is using Intersect with null elements. The default equality comparer handles null as a valid value. If a sequence contains null, it will be compared correctly. However, if you write a custom comparer, you must handle null arguments in Equals and GetHashCode to avoid NullReferenceException. The comparer should treat two null references as equal and return a consistent hash code for null (commonly 0).
Also, note that Intersect does not throw when the sequences are null. It will throw ArgumentNullException if either source is null. This is consistent with other LINQ methods.
Finally, consider the case where the sequences are of different types. Intersect requires both sequences to be IEnumerable<T> of the same type T. If you have IEnumerable<int> and IEnumerable<long>, you need to cast one to the other or use Cast<T> to align the types. This is a compile-time requirement, not a runtime behavior.
For large data sets, the hash-based approach can be memory-heavy. If you are working with database-backed sequences (e.g., EF Core), Intersect is translated to SQL's INTERSECT when the sequences are IQueryable. In that case, the operation is performed on the database side, and the memory considerations differ. Be aware of the difference between LINQ to Objects and LINQ to Entities.
Implementing a Custom Intersect for Multiset Semantics
If you genuinely need a multiset intersection, you can implement it with a Dictionary<T, int> that counts occurrences. Here is a straightforward approach:
public static IEnumerable<T> MultisetIntersect<T>(this IEnumerable<T> first, IEnumerable<T> second) { var counts = new Dictionary<T, int>(); foreach (var item in first) { counts.TryGetValue(item, out var count); counts[item] = count + 1; } var result = new List<T>(); foreach (var item in second) { if (counts.TryGetValue(item, out var count) && count > 0) { result.Add(item); counts[item] = count - 1; } } return result; }
This method preserves the number of occurrences that exist in both sequences. For example, [1, 1, 2] intersected with [1, 2, 2] yields [1, 2]. The standard Intersect would also yield [1, 2] because it returns distinct elements. The difference becomes visible when you have multiple duplicates: [1, 1, 2] intersected with [1, 1, 1, 2] yields [1, 1, 2] with the custom method, while the standard Intersect returns [1, 2].
This custom implementation uses the default equality comparer. If you need custom equality, you can add an IEqualityComparer<T> parameter to the Dictionary constructor. The method is an extension method, so you can call it on any IEnumerable<T>.
Keep in mind that this implementation is not lazy; it materializes the first sequence into a dictionary and the result into a list. That is acceptable for most scenarios where multiset semantics are required, but it is worth noting if you are working with very large sequences.