Back to Blog
C#

C# List Find: Using Predicates to Locate Elements

c# list find: Learn how to use List<T>.Find with predicates, handle missing elements, and decide when to prefer LINQ alternatives.

C#List<T>PredicateLINQFirstOrDefault
Illustration of searching a C# list with a predicate to find a matching element.

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

When you need to locate a specific element in a List<T>, the Find method is one of the most direct tools in C#. It accepts a predicate and returns the first element that matches, or the default value if none is found. This article covers how to use Find effectively, how it behaves in edge cases, and when other approaches like FirstOrDefault or FindAll are better suited.

The Find Method Signature and Basic Usage

The List<T>.Find method is defined on the List<T> class and takes a Predicate<T> delegate. The predicate is a method that receives each element and returns a boolean indicating whether that element matches the search condition. The method returns the first element for which the predicate returns true, scanning the list from index zero upward.

List<int> numbers = new List<int> { 10, 20, 30, 40 }; int firstEven = numbers.Find(n => n % 2 == 0); Console.WriteLine(firstEven); // outputs 10

The predicate is often written as a lambda expression, but it can also be a named method or a method group. The method returns the element itself, not its index. If you need the index, FindIndex is the corresponding method.

How Find Handles Missing Elements

If no element satisfies the predicate, Find returns the default value for the type T. For reference types and nullable value types, that default is null. For non-nullable value types like int, it is 0. This behavior is consistent with the rest of the List<T> search family, but it can lead to subtle bugs if you forget to check for the default.

List<string> names = new List<string> { "Alice", "Bob" }; string result = names.Find(n => n.StartsWith("Z")); Console.WriteLine(result == null); // outputs True

Always verify the result against the default before using it, especially when the default is a valid element value. For example, finding a 0 in a list of integers is indistinguishable from a failed search unless you explicitly check the list content or use a different approach.

Using a Predicate with Find

A predicate can capture variables from the surrounding scope, making it flexible for dynamic search criteria. This is useful when the condition depends on external input.

int threshold = 25; List<int> values = new List<int> { 5, 15, 25, 35 }; int firstAboveThreshold = values.Find(v => v > threshold); Console.WriteLine(firstAboveThreshold); // outputs 35

You can also use a method group if you already have a method that matches the Predicate<T> signature. This keeps the code readable when the logic is complex enough to warrant a named method.

List<Customer> customers = GetCustomers(); Customer activeCustomer = customers.Find(IsActive); static bool IsActive(Customer c) => c.Status == CustomerStatus.Active;

The predicate is evaluated sequentially until a match is found. It is not evaluated for elements after the first match, which is an important performance characteristic for large lists.

Find vs. FirstOrDefault: What's Different?

Find is a method on List<T>, while FirstOrDefault is a LINQ extension method that works on any IEnumerable<T>. Both return the first matching element or the default value, but they differ in several practical ways.

AspectList<T>.FindEnumerable.FirstOrDefault
Target typeList<T> onlyAny IEnumerable<T>
Predicate typePredicate<T>Func<T, bool>
ImplementationDirect loop in List<T>LINQ iterator, often with deferred execution
OverheadSlightly lower, no iterator state machineSlightly higher due to iterator allocation
Availability.NET Framework 2.0+.NET 3.5+ (LINQ)

In most applications the performance difference is negligible. The choice often comes down to whether you are already working with a List<T> or a more general sequence. If you have a List<T> and want the simplest syntax, Find is natural. If you are working with an IEnumerable<T> or want to chain other LINQ operators, FirstOrDefault is the better fit.

Performance Considerations for Find

Find performs a linear scan from the beginning of the list. Its time complexity is O(n) in the worst case, where n is the number of elements. This is the same as FirstOrDefault and any other sequential search. For a one-time lookup, this is usually acceptable. However, if you find yourself calling Find repeatedly on the same list with different predicates, consider whether a different data structure would be more efficient.

For example, if you need to look up elements by a unique key, a Dictionary<TKey, TValue> provides O(1) average-case lookup. If you need to maintain order and also perform frequent searches, a sorted list with binary search might be worth considering, though List<T> does not expose a built-in binary search for predicates.

The predicate itself can also affect performance. If the predicate performs expensive work, the total cost grows with the number of elements examined. In the worst case, every element is tested. If you know the list is sorted, you can sometimes break early manually, but Find does not take advantage of ordering.

Common Pitfalls with Find

One common mistake is modifying the list while Find is executing. If the predicate adds or removes elements from the same list, it can cause unexpected behavior or throw an InvalidOperationException because the list's internal version changes. Avoid side effects inside the predicate.

Another pitfall is passing a null predicate. Find throws an ArgumentNullException if the predicate is null. Always ensure the predicate is non-null before calling Find.

Also, remember that Find returns the first match. If multiple elements satisfy the condition, only the first one is returned. If you need all matches, use FindAll instead. FindAll returns a new List<T> containing every element that matches the predicate.

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 }; List<int> evens = numbers.FindAll(n => n % 2 == 0); // evens contains 2, 4, 6

When to Prefer FindAll or LINQ Instead

FindAll is useful when you need a list of all matches, not just the first. It creates a new list, so it has additional memory overhead compared to a simple loop that collects matches into a list manually. For large collections, consider whether you need the results as a List<T> or if an iterator-based approach like Where would be more memory-efficient.

LINQ's Where returns a lazy IEnumerable<T>, which can be more efficient if you only need to iterate the results once or if you plan to apply further transformations. However, if you need a concrete List<T> and are already working with a List<T>, FindAll is straightforward and avoids the extra LINQ overhead.

For the common case of finding a single element, Find is a clear and readable choice when you have a List<T>. If you are working with an array or another collection type, FirstOrDefault is the idiomatic LINQ equivalent. The decision should be based on the type you have and whether you need deferred execution or a materialized result.

Handling Value Type Defaults Correctly

When T is a non-nullable value type, Find returns 0 (or the default struct value) when no match is found. This can be ambiguous if 0 is a valid element. To avoid this ambiguity, consider using FindIndex and checking the returned index, or use a nullable wrapper if you need to distinguish "not found" from a valid default value.

List<int> numbers = new List<int> { 0, 1, 2 }; int found = numbers.Find(n => n > 5); // returns 0, indistinguishable from a valid element bool exists = numbers.Exists(n => n > 5); // returns false, clearer for existence checks

For existence checks, Exists is a more direct method because it returns a boolean. Use Find when you need the actual element, and use Exists when you only need to know whether a match exists. This keeps the intent of the code explicit and avoids default-value confusion.

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