List and HashSet in C# can both hold values, but they describe those values differently. A list is a sequence with positions and possible duplicates. A set represents unique membership according to an equality rule. That difference matters before performance enters the discussion.
Suppose a screen displays submitted product codes. Repeated submissions might be important history, while a separate validation step only needs to know whether each code has appeared before. Keeping duplicates and rejecting duplicates are both reasonable behaviors. They answer different questions.
This article explains the sequence-versus-set distinction, then follows it into equality, stable identifiers, and ordered deduplication. It does not replace the existing Dictionary guides or present a benchmark ranking. The complete console examples use C# 12-compatible syntax and .NET 10 APIs; they are documented examples, not executed measurements.
List and HashSet in C# Represent Different Information
Microsoft defines List as a collection accessible by index that allows duplicate elements. Indexes are zero-based. A list retains a current sequence, but it is not automatically sorted.
Microsoft describes HashSet elements as having no particular order. The set uses equality to avoid duplicate members. It does not supply a positional indexer comparable to a list's.
| Requirement | What a list communicates | What a set communicates |
|---|---|---|
| Preserve every submitted occurrence | Duplicates are permitted | Equal members collapse into one membership |
| Read the item at position two | Indexed access is available | Position is not part of the set contract |
| Determine whether a value exists | Search the sequence | Query membership under the comparer |
| Preserve a display sequence | Current list positions matter | Establish ordering separately |
| Express uniqueness | Requires an additional rule | Uniqueness is built into membership |
Neither column is universally superior. If repeated codes represent repeated purchases, discarding duplicates changes the data. If repeated codes represent redundant permission names, retaining them might obscure the model instead.
The first useful question is therefore not “Which one is faster?” It is “What does one occurrence mean?” That answer determines whether duplication is information to preserve or redundancy to remove.
A List Preserves Occurrences and Positions
Consider a simple audit display. Every submitted value should remain visible, including an exact repeat. The screen also needs positional reading because it highlights the second submission.
using System;
using System.Collections.Generic;
var submissions = new List<string> { "P-100", "P-200", "P-100" };
Console.WriteLine($"Occurrences: {submissions.Count}");
Console.WriteLine($"Second submission: {submissions[1]}");
submissions.Insert(0, "P-050");
Console.WriteLine($"New second submission: {submissions[1]}");
Console.WriteLine(string.Join(", ", submissions));
The inserted entry changes the positions of subsequent entries. That is appropriate for a sequence, but it shows why a list index should not be treated as a permanent product identity.
An identifier and a position answer different questions. The identifier names an entity; the position names where an entry currently appears. An application that stores “product at index one” elsewhere can point to a different product after an insertion.
A list can be explicitly sorted when presentation requires it. Sorting changes the sequence, so arrival order and alphabetical order should not be conflated. If both are important, retain the original sequence and create a separately ordered representation for the display.
The foreach guide explains how to traverse collections. Traversal alone does not decide whether current list order represents arrival time, priority, or another application rule.
A HashSet Expresses Membership Under a Comparer
A set is useful when the application asks whether a value belongs to a group. The membership rule includes equality. Two values can be different spellings while still being the same member under a chosen comparer.
using System;
using System.Collections.Generic;
var approvedCodes = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
Console.WriteLine(approvedCodes.Add("P-100"));
Console.WriteLine(approvedCodes.Add("p-100"));
Console.WriteLine(approvedCodes.Add("P-200"));
Console.WriteLine($"Members: {approvedCodes.Count}");
Console.WriteLine($"Contains P-100: {approvedCodes.Contains("P-100")}");
Console.WriteLine($"Contains P-300: {approvedCodes.Contains("P-300")}");
Add reports whether it inserted a new member. The differently cased second code is equal under this example's explicit case-insensitive policy, so it does not become a second member. That result is about identity, not string normalization.
The set is not rewriting every stored string into a standard case. If a downstream interface requires uppercase codes, normalize them as a separate policy. Membership comparison and display formatting should not accidentally substitute for each other.
The comparer also needs to match the domain. A machine identifier with case-insensitive semantics can reasonably use an ordinal case-insensitive comparer. A case-sensitive access token cannot. Applying one comparer everywhere because it is convenient can silently broaden what the application accepts as equal.
This is closely related to Dictionary key selection: both keyed lookup and set membership depend on an equality policy. A dictionary additionally associates a value with each key, while a set represents membership without that associated value.
Equality and Ordering Are Separate Policies
Equality asks whether two values represent the same member. Ordering asks which value should appear before another. A case-insensitive equality policy does not automatically establish a display sort.
It is also possible for ordering and equality to use different rules for a legitimate reason. A screen may sort names for readability while uniqueness is determined by an immutable account identifier. The collection code should make those two decisions visible rather than allowing a name comparison to redefine identity.
Microsoft's List documentation separates equality-based methods such as Contains from ordering-based methods such as Sort. The fact that both operate on the same element type does not make their comparison contracts interchangeable.
For a set, the absence of an ordering contract is important even if a small example repeatedly prints the same order. Repeatability in one input and runtime configuration is not the same as a documented insertion-order guarantee.
When ordered output is required, obtain it from an explicitly ordered representation. The set should answer the membership question. The sequence should answer the presentation question. This makes later changes to display ordering less likely to disturb equality semantics.
Stable Identifiers Protect Hash-Based Membership
Hash-based collections locate candidates using hash values and distinguish equality using their comparers. If equality-relevant data changes while an object is stored, the application's lookup assumptions can become unreliable.
Microsoft's Dictionary documentation explicitly requires a key not to change in a way that affects its hash value. The same practical stability concern applies when a hash set stores objects under hash-based equality: use a membership identity that remains stable while stored.
A string identifier is a straightforward option because its contents are immutable. A value-oriented record made only of suitable stable fields can also be useful. Neither approach requires every object in the application to become immutable; it requires the identity used for membership to remain appropriate.
using System;
using System.Collections.Generic;
var selectedProducts = new HashSet<ProductId>
{
new ProductId("catalog", 100),
new ProductId("catalog", 200)
};
var sameProduct = new ProductId("catalog", 100);
Console.WriteLine(selectedProducts.Contains(sameProduct));
var renamedDisplayLabels = new Dictionary<ProductId, string>
{
[sameProduct] = "Updated display name"
};
Console.WriteLine(renamedDisplayLabels[sameProduct]);
public sealed record ProductId(string Catalog, int Number);
The product identifier and the display label are separate. Changing the associated display label does not change the set's stored identifier. This models a common application distinction: names can change while identity remains stable.
The record's generated value equality includes its components. Here those components are a string and an integer, making the example's identity straightforward. A record with a nested mutable object would require another look at that object's equality and mutation behavior.
The Dictionary variations article gives additional context for custom keys. The lesson for a set is the same boundary: decide identity before choosing how to store and query it.
Ordered Deduplication Uses Two Responsibilities
Sometimes a caller needs the first occurrence of each identifier in arrival order. A set alone does not promise that output order. A list alone does not enforce uniqueness. The responsibilities can be combined explicitly.
using System;
using System.Collections.Generic;
var source = new[] { "P-200", "P-100", "p-200", "P-300", "P-100" };
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var firstOccurrences = new List<string>();
foreach (var code in source)
{
if (seen.Add(code))
{
firstOccurrences.Add(code);
}
}
Console.WriteLine(string.Join(", ", firstOccurrences));
The set answers whether the identifier has appeared. The list stores the first accepted occurrence in the source's traversal order. Nothing in the output depends on enumerating the set.
This policy retains the first spelling encountered. If the application needs a canonical spelling, make that a separate transformation. If it needs the latest occurrence rather than the first, the algorithm needs another policy instead of a changed explanation.
There are tradeoffs. Two structures require additional storage and a clear ownership boundary. In this example they are local to one operation, so the synchronization problem of maintaining them across multiple writers does not arise.
Persisting both as shared mutable state is a different design. Every addition and removal would need to maintain both consistently. A temporary result assembled from one source is simpler than a long-lived pair of indexes unless the application actually needs the latter.
Complexity Is Not a Stopwatch Result
Microsoft describes HashSet Contains as an O(1) operation. Read that as an algorithmic characterization, not a promise of identical duration for every key, set size, comparer, or runtime environment.
Hashing and equality still perform work. An expensive comparer can make an apparently simple membership query expensive. Collision behavior and implementation details can also affect the work behind a lookup.
List membership search examines the sequence rather than using a hash-based membership index. Its usefulness still depends on the workload. If a caller already needs a short ordered sequence and only asks one membership question, building a second structure is not automatically justified.
Conversely, repeated membership queries over a large group may make the set abstraction important. That is a workload hypothesis to validate in the application, not evidence of a specific speedup from these examples.
No timing, allocation percentage, or universal fastest-collection claim follows from this article. The collection contract is the starting point; measurement is a separate activity with specified data, operations, runtime, and environment.
Mutation and Ownership Still Matter
Both List and HashSet are mutable collections. Returning either through IEnumerable<T> does not freeze it. The IEnumerable guide explains the narrower traversal surface, but another alias can still modify a mutable source.
Likewise, a copied outer collection is not automatically a deep copy. If its elements are references to mutable objects, the copies can still share those objects. An immutable identifier reduces one risk without establishing that every surrounding object is immutable.
Changes during enumeration must be evaluated against the actual collection's contract. Do not assume foreach creates a snapshot, and do not generalize one mutation rule to all collections and versions.
The iterator and collection traps are useful context when a sequence's materialization or repeated consumption changes its costs. The present distinction remains narrower: what information does membership preserve, and what information does a sequence preserve?
Changing the Container Can Change the Meaning
Consider importing purchase lines. Two lines with the same product identifier might represent two separate purchases, two quantities that should be combined, or a duplicate transmission that should be ignored. All three situations can contain the same raw identifier strings.
A list preserves the occurrences so a later operation can interpret them. A set immediately establishes membership uniqueness, which can discard occurrence information before the application has decided whether that information matters. The container change is therefore a semantic change, not merely an optimization.
If lines should be combined, membership alone is insufficient because the result also needs an associated quantity. That is an association problem and belongs with a keyed model. This article does not extend the set example into another Dictionary tutorial; it simply identifies the point where the reader's question changes.
The same reasoning applies to display requirements. An arrival-ordered list can show what was received. A sorted list can show a presentation order. A set can answer which identifiers occurred. Preserving all three views may be intentional, but each should have an owner and a reason to exist.
When requirements change, reconsider the equality policy as well as the container. Switching from case-sensitive to case-insensitive membership can merge previously distinct identifiers even if the collection type remains exactly the same. That is an observable change for callers.
Examples should make these policies visible with inputs that include a repeat and a near-match. A demonstration containing only distinct values can conceal the primary difference between a list and a set. Likewise, checking only one set enumeration can conceal the absence of an ordering contract.
The useful mental model is not “a set is a faster list.” It is “a set records membership and a list records a sequence.” Once that distinction is clear, application requirements can explain whether either structure, or a bounded combination, is appropriate.
Frequently Asked Questions
Can List contain duplicates?
Yes. Duplicate entries are allowed and can carry useful information, such as repeated events. Deduplication is an application policy rather than a default list behavior. If occurrences matter, replacing the list with a set can lose information.
Is HashSet ordered by insertion?
Do not use it as an insertion-ordered collection. Its documented model has no particular order. Keep a separate sequence for an ordered result or establish explicit sorting before displaying members.
Can a HashSet use case-insensitive string equality?
Yes, by supplying an appropriate comparer such as StringComparer.OrdinalIgnoreCase. Choose it because the domain's identifiers are case-insensitive, not because case-insensitive behavior is universally safer or more convenient.
Should mutable objects be used as set members?
They can be, but equality and hash behavior must remain stable for reliable membership. Mutable display properties are less concerning when they do not participate in identity. Stable identifier values often make that boundary easier to understand.
Is HashSet always faster than List?
No universal elapsed-time ranking follows from their contracts. The operation mix, data size, hashing, comparisons, and construction costs matter. A set communicates membership semantics; a list communicates sequence semantics. Measure a relevant workload when performance is material.
How can duplicates be removed without losing arrival order?
Track seen identifiers in a set and append first occurrences to a list while traversing the source. This makes the equality rule and output order explicit. It also makes the first-occurrence policy visible instead of relying on set enumeration.
Understanding List and HashSet in C# Clearly
A list preserves positions and occurrences. A set preserves membership under a comparer. Equality, ordering, and ownership are separate decisions that should remain visible in application code.
Once those decisions are clear, the container no longer looks like an interchangeable implementation detail. It explains what the data means and which changes would alter that meaning.

