C# List Contains: Checking for Elements in a List
c# list contains: Learn how List<T>.Contains works in C#, how equality is determined for value and reference types, and when HashSet is more efficient for membership c...
Checking whether a C# list contains a specific value is a common task, and the Contains method is the most direct way to do it. List<T>.Contains(item) returns true when an equal element exists in the list, and false otherwise. The method is part of the List<T> class itself, so no LINQ import is required.
How List<T>.Contains Determines a Match
The behavior of Contains depends entirely on how equality is defined for the type T. For value types such as int, double, or a struct, the method compares values directly. For reference types such as string or custom classes, the default behavior compares references unless the type overrides Equals.
List<string> fruits = new List<string> { "apple", "banana", "cherry" }; bool hasBanana = fruits.Contains("banana"); Console.WriteLine(hasBanana); // True
The string comparison works because string overrides Equals to compare character content. The same code with a custom class behaves differently, as shown later in this article.
Contains with Value Types
For value types, Contains performs a straightforward value comparison. Each element in the list is compared with the target using the type's default equality, which for built-in numeric and boolean types means a direct value check.
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 }; bool hasThree = numbers.Contains(3); // True bool hasTen = numbers.Contains(10); // False
This is the simplest case. There is no ambiguity about what "equal" means, and the behavior is predictable across all value types that do not override equality themselves.
Contains with Reference Types
With reference types, the default equality check is reference equality. Two separate instances that contain identical data are not considered equal unless the type overrides Equals and GetHashCode.
public class Person { public string Name { get; set; } } List<Person> people = new List<Person>(); Person alice = new Person { Name = "Alice" }; people.Add(alice); bool sameReference = people.Contains(alice); // True bool newInstance = people.Contains(new Person { Name = "Alice" }); // False
The second call returns false because the new Person object is a different reference, even though its Name property holds the same string. This is a common source of confusion when developers expect Contains to compare object properties.
Custom Objects: Overriding Equality
To make Contains compare the contents of custom objects, override Equals and GetHashCode on the type. List<T>.Contains uses the default equality comparer, which calls Equals on the objects being compared.
public class Person { public string Name { get; set; } public override bool Equals(object obj) { return obj is Person other && Name == other.Name; } public override int GetHashCode() { return Name.GetHashCode(); } }
With this override in place, the earlier example changes behavior:
List<Person> people = new List<Person>(); people.Add(new Person { Name = "Alice" }); bool found = people.Contains(new Person { Name = "Alice" }); // True
The GetHashCode override matters even though Contains does not use a hash-based lookup. The .NET equality contract requires that two objects considered equal by Equals produce the same hash code. Violating that contract breaks other collection types such as Dictionary<TKey, TValue> and HashSet<T>, which do rely on hash codes.
If you cannot modify the class, an alternative is to use List<T>.Exists with a predicate or LINQ's Any method, both of which let you define the comparison inline without changing the type.
Performance: Linear Search Cost
List<T>.Contains performs a linear scan. It iterates through the list from the first element until it finds a match or reaches the end. The time complexity is O(n), where n is the number of elements in the list. For small lists this cost is negligible, but it becomes noticeable when the list grows large and Contains is called frequently, such as inside a loop.
// Called once per iteration of an outer loop foreach (var id in incomingIds) { if (existingIds.Contains(id)) { // handle duplicate } }
If existingIds has thousands of elements and incomingIds has thousands too, the total work becomes O(n × m). That quadratic behavior is the most common reason developers replace List<T> with a hash-based collection.
When a HashSet Is the Better Choice
When you only need to check membership and do not care about the order of elements, HashSet<T> provides O(1) average lookup time. The Contains method on HashSet<T> uses the hash code of the item to locate it directly instead of scanning the entire collection.
HashSet<string> fruitSet = new HashSet<string> { "apple", "banana", "cherry" }; bool hasBanana = fruitSet.Contains("banana"); // O(1)
The tradeoff is that HashSet<T> does not preserve insertion order and does not allow duplicate elements. If you need to keep duplicates or maintain a specific order, a List<T> remains necessary, and you must accept the linear scan cost or use a secondary index structure.
A practical middle ground is to maintain both a List<T> for ordered iteration and a HashSet<T> for membership checks, keeping them synchronized when items are added or removed. This adds complexity, so it is only worth doing when profiling shows that Contains on the list is an actual bottleneck.
Contains vs Any vs Exists
The List<T> class also exposes Exists, which accepts a Predicate<T>, and LINQ provides Any, which accepts a Func<T, bool>. Both allow you to express a comparison that is not based on default equality.
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 }; bool hasEven = numbers.Exists(x => x % 2 == 0); // True bool hasEvenLinq = numbers.Any(x => x % 2 == 0); // True
Exists is a method on List<T> itself and works without importing System.Linq. Any is a LINQ extension method that also works on IEnumerable<T>, so it is the right choice when your code operates on an interface or a different collection type. When the comparison is simply default equality, Contains is clearer than writing a lambda that calls Equals.
Null Handling and Edge Cases
List<T>.Contains(null) returns true when the list contains a null element. This is valid for reference types and for nullable value types.
List<string> items = new List<string> { "a", null, "b" }; bool hasNull = items.Contains(null); // True
For a list of non-nullable value types, Contains(null) does not compile because int cannot be assigned null. If you work with a List<int?>, the null check behaves the same as with reference types.
One additional edge case is floating-point comparison. Contains uses the default equality comparer, which means double.NaN is treated as equal to itself inside a list, while the == operator returns false for NaN. This subtle difference can surprise developers who expect Contains to behave like ==.
List<double> values = new List<double> { double.NaN }; bool containsNaN = values.Contains(double.NaN); // True bool equalsNaN = double.NaN == double.NaN; // False
This behavior comes from EqualityComparer<double>.Default, which follows object.Equals semantics rather than the == operator. When you need the == behavior instead, use Any(x => x == double.NaN) or an equivalent predicate.