C# HashSet UnionWith: In-Place Set Union Explained
c# hashset unionwith: Learn how HashSet<T>.UnionWith merges elements in place, how it differs from LINQ's Union, and when to use it for efficient set merging.
c# hashset unionwith requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
What HashSet<T>.UnionWith Does
HashSet<T>.UnionWith is an instance method that modifies the set in place by adding all elements from another collection that are not already present. It implements the mathematical union operation: the resulting set contains every element that appears in either the original set or the supplied collection.
var primary = new HashSet<int> { 1, 2, 3 }; var secondary = new HashSet<int> { 3, 4, 5 }; primary.UnionWith(secondary); // primary now contains { 1, 2, 3, 4, 5 }
The method returns void. It does not create a new set. The caller's set is mutated directly. This is the most important behavioral distinction to understand before using it.
Syntax and Parameters
The method signature is:
public void UnionWith(IEnumerable<T> other)
The parameter accepts any IEnumerable<T>, not just another HashSet<T>. You can pass a List<T>, an array, a SortedSet<T>, or any other collection that implements the interface.
var codes = new HashSet<string> { "A1", "B2" }; var incoming = new List<string> { "B2", "C3", "D4" }; codes.UnionWith(incoming); // codes now contains { "A1", "B2", "C3", "D4" }
Because the parameter is IEnumerable<T>, the method will enumerate the entire source collection. If the source is a lazy sequence (for example, a LINQ query that performs deferred execution), the enumeration happens when UnionWith is called.
Duplicate Handling
UnionWith relies on the set's equality comparer to determine whether an element already exists. By default, that is EqualityComparer<T>.Default, which uses T.Equals and T.GetHashCode.
When the method encounters an element already present in the set, it skips it. When it encounters a new element, it adds it. The result never contains duplicates, regardless of how many duplicates exist in the source collection.
var numbers = new HashSet<int> { 10, 20 }; var source = new List<int> { 10, 10, 20, 30, 30, 30 }; numbers.UnionWith(source); // numbers contains { 10, 20, 30 }
For reference types, equality is determined by the comparer. If you need custom equality semantics, you can pass a custom IEqualityComparer<T> when constructing the HashSet<T>, and UnionWith will respect it.
Difference from LINQ's Union
LINQ's Enumerable.Union is the closest conceptual relative, but the behavior differs in a way that matters in practice:
| Behavior | HashSet<T>.UnionWith | Enumerable.Union |
|---|---|---|
| Mutates the original set | Yes | No |
| Returns a new collection | No | Yes |
| Return type | void | IEnumerable<T> |
| Deferred execution | No | Yes |
Requires a HashSet<T> instance | Yes | No |
var set = new HashSet<int> { 1, 2 }; var other = new List<int> { 2, 3 }; var unionResult = set.Union(other); // new IEnumerable<int>, set unchanged set.UnionWith(other); // set mutated in place
Enumerable.Union is the right choice when you need to combine two sequences without mutating either one, or when the source is not a HashSet<T>. UnionWith is the right choice when you are building up a set incrementally and want to avoid allocating a new collection on every merge.
Performance and Allocation Behavior
Because UnionWith mutates the existing set, it avoids allocating a new HashSet<T> for the result. When a set is being merged repeatedly in a loop, this matters:
var allIds = new HashSet<int>(); foreach (var batch in batches) { allIds.UnionWith(batch); // reuses the same set instance }
Each call to UnionWith still enumerates the incoming collection and performs a hash lookup per element. The cost per element is the same as an Add call, but the set's internal storage is reused. If you used Enumerable.Union in a loop and converted the result back to a HashSet<T> each time, you would allocate a new set per iteration and copy all existing elements repeatedly, which increases total work as the set grows.
The in-place behavior also matters when other code holds a reference to the set. Because the same instance is mutated, all references observe the updated contents. With Enumerable.Union, the original set stays unchanged, which can be desirable or surprising depending on the context.
Edge Cases and Failure Conditions
UnionWith throws ArgumentNullException if the other parameter is null. An empty collection is valid and leaves the set unchanged.
var set = new HashSet<int> { 1, 2 }; set.UnionWith(new List<int>()); // set remains { 1, 2 }
Calling UnionWith on an empty set is equivalent to adding every element from the source:
var empty = new HashSet<int>(); empty.UnionWith(new[] { 5, 6, 7 }); // empty now contains { 5, 6, 7 }
There is no special handling for the set being the same object as the source. If you pass the set itself, the method enumerates the set and adds elements that are already present, which results in no change.
Custom Equality Comparers
When the HashSet<T> is constructed with a custom comparer, UnionWith uses that comparer for both duplicate detection and element insertion. This is useful when elements are considered equal by a subset of their properties.
public record Product(int Id, string Name); public class ProductIdComparer : IEqualityComparer<Product> { public bool Equals(Product x, Product y) => x.Id == y.Id; public int GetHashCode(Product obj) => obj.Id.GetHashCode(); } var products = new HashSet<Product>(new ProductIdComparer()) { new Product(1, "Keyboard"), new Product(2, "Mouse") }; var incoming = new List<Product> { new Product(2, "Trackpad"), // same Id, different Name new Product(3, "Monitor") }; products.UnionWith(incoming); // products contains Ids 1, 2, 3 // The Product with Id 2 keeps the original "Mouse" entry
The comparer determines which elements are considered duplicates. When an incoming element matches an existing one, the existing element is retained; the incoming element is not inserted. This behavior is worth noting when the incoming data carries newer values that should replace old ones — UnionWith does not perform replacement.
When to Use UnionWith vs Other Approaches
Use UnionWith when you already have a HashSet<T> and want to add elements from another collection without allocating a new set, and when mutation of the original set is intended and safe.
Use Enumerable.Union when you need to combine two sequences without modifying either one, when the result should be lazily evaluated, or when you are working with IEnumerable<T> sources that are not sets.
Use a loop with Add when you need to inspect or handle each element before adding it, or when the logic for skipping elements is more complex than simple union semantics.
The choice is driven by whether the original set should be mutated and whether allocation cost matters in the calling context. For a one-off merge where the original set can be discarded, Enumerable.Union is simpler and avoids surprising side effects. For incremental accumulation where the set is reused across many operations, UnionWith avoids repeated allocation and copying.