Back to Blog
C#

C# List Exists: How to Check for Elements

c# list exists: Learn how to check if an item exists in a C# List using Contains, Exists, and LINQ Any, with performance and equality considerations.

C#ListLINQContainsPredicate
Illustration of a C# List with a magnifying glass checking for an element, representing existence checks.

c# list exists requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

When you need to determine whether a value already exists in a C# List, the framework offers several APIs with different semantics. The most direct is List.Contains, but for more complex conditions you have List.Exists and LINQ's Any. Each behaves differently in terms of equality, allocation, and performance, so the right choice depends on the data and the condition you are testing.

Checking for an Element with List.Contains

The simplest way to check for an element in a List<T> is the Contains method. It uses the default equality comparer for the type T and returns true if any element matches the specified value.

var numbers = new List<int> { 1, 2, 3, 4, 5 }; bool hasThree = numbers.Contains(3); // true bool hasTen = numbers.Contains(10); // false

Contains performs a linear scan from the beginning of the list until it finds a match or reaches the end. For value types like int, the comparison is straightforward. For reference types, it uses EqualityComparer<T>.Default, which calls Equals unless the type overrides it. This means that for a custom class without an Equals override, Contains checks reference equality, not structural equality.

Using List.Exists with a Predicate

When you need to check for an element that satisfies a condition beyond simple equality, List<T>.Exists is a direct option. It accepts a Predicate<T> and returns true if any element meets the condition.

var users = new List<User> { /* ... */ }; bool hasAdmin = users.Exists(u => u.Role == "Admin");

The predicate receives each element in turn, and the method stops as soon as the predicate returns true. This is useful for checking properties, ranges, or any custom logic that Contains cannot express. Exists is specific to List<T> and does not exist on other collection types like arrays or IEnumerable<T>.

Using LINQ's Any Method

LINQ provides the Any extension method, which works on any IEnumerable<T>, including List<T>. Like Exists, it takes a predicate and returns true if any element satisfies it.

var users = new List<User> { /* ... */ }; bool hasInactive = users.Any(u => u.Status == "Inactive");

Any also has a parameterless overload that simply checks if the sequence contains at least one element. When you pass a predicate, the behavior is equivalent to Exists for a List<T>, but Any is more general and can be used with any LINQ query result, such as a filtered or projected sequence.

Comparing Contains, Exists, and Any

All three methods perform a linear scan and have O(n) time complexity, but they differ in how they evaluate matches and where they are available.

MethodTargetConditionEqualityAvailability
ContainsList<T>Value equalityEqualityComparer<T>.DefaultInstance method
ExistsList<T>PredicateCustom logicInstance method
AnyIEnumerable<T>Predicate or empty checkCustom logicExtension method

For simple value checks, Contains is the most readable. For predicate-based checks on a List, Exists is slightly more direct because it does not require a using System.Linq directive. However, Any is the standard choice when you are already working with LINQ or when the collection might not be a List in future refactoring.

Performance and Allocation Behavior

The runtime cost of these checks is dominated by the linear scan. For a list of n elements, each method compares elements until a match is found, so the worst case is n comparisons. The predicate-based methods add a delegate invocation per element, which is negligible for most applications but can matter in tight loops over very large lists.

One important allocation difference is that Contains does not allocate a delegate, while Exists and Any require a predicate delegate. If you create a new lambda expression inside a loop, the compiler may cache it for static methods, but if the lambda captures local variables, a new closure object is allocated each iteration. This can increase memory pressure in high-frequency code paths.

If you need to check for the existence of many distinct values in the same list, a HashSet<T> is usually a better choice. Building the set once gives O(1) lookups, whereas repeated Contains calls on a list are O(n) each. The tradeoff is the upfront cost of building the set and the extra memory it consumes.

Custom Types and Equality Semantics

Contains relies on the type's Equals and GetHashCode implementations. For a custom class, the default behavior is reference equality, which often does not match what you want. Consider this example:

public class Product { public int Id { get; set; } public string Name { get; set; } } var productList = new List<Product>(); var target = new Product { Id = 1, Name = "Laptop" }; bool exists = productList.Contains(target); // false unless same reference

To make Contains work by value, override Equals and GetHashCode in the class, or use a custom IEqualityComparer<T> with the Contains overload that accepts one. In contrast, Exists and Any let you compare specific properties directly without modifying the class:

bool exists = productList.Exists(p => p.Id == target.Id); bool existsLinq = productList.Any(p => p.Id == target.Id);

This is often simpler when the equality rule is only needed for one check and does not represent the natural identity of the object.

Edge Cases and Null Handling

All three methods handle null elements according to the predicate or equality logic. Contains uses the default equality comparer, which handles null correctly for reference types. If the list contains a null element and you call Contains(null), it returns true. For Exists and Any, the predicate must explicitly check for null if that is a valid condition.

An empty list always returns false for all three methods, because there are no elements to match. The parameterless Any() returns false for an empty sequence, which is a quick way to check if a list has any items without using the Count property.

When the predicate itself throws an exception, the behavior depends on where the exception occurs. Exists and Any stop iterating at that point and propagate the exception. This is the same as any other delegate invocation and should be handled by the caller.

Choosing the Right Existence Check

Use Contains when you are checking for a single value and the type's default equality semantics are correct. It is the most readable and does not require a delegate allocation.

Use Exists when you need a predicate and you are working directly with a List<T>. It is slightly more concise than Any because it does not require a LINQ import, and it clearly signals that the target is a list.

Use Any when you are already using LINQ, when the collection might change to another IEnumerable<T> implementation, or when you need the parameterless overload to check for non-emptiness. Any is also the natural choice in query expressions where you are chaining multiple LINQ operations.

For repeated existence checks against the same collection, consider converting the list to a HashSet<T> once and using Contains on the set. This changes the lookup from O(n) to O(1) and is worth the extra memory when the list is large and the check happens frequently. The conversion itself is O(n), so the benefit only appears after several lookups.

When you override Equals on a class, remember to also override GetHashCode to maintain the contract used by Contains and other collection operations. A common mistake is to override only Equals, which causes incorrect behavior in hash-based collections and can lead to subtle bugs in Distinct, HashSet, and dictionary lookups.

Finally, be aware that Exists and Any are not interchangeable in all contexts. Exists is an instance method on List<T> and is not available on arrays or other collection types. If you later change the variable type from List<T> to IEnumerable<T>, you must switch to Any. Writing the code with Any from the start makes that refactoring easier.

c# list exists: Practical Usage and Code Examples | RYUSLOG DEV