Back to Blog
C#

C# Where vs Find: Choosing the Right Approach

Understand the differences between Where and Find in C#, including when each is appropriate for collection searches and how their behavior affects performance.

LINQList<T>ArrayPerformanceC#
Illustration comparing C# List Find and LINQ Where methods, showing a single element versus multiple elements

In C#, when you work with collections, Where(predicate) and Find(predicate) are common ways to search with a predicate, but they are not interchangeable. Where is a LINQ extension method that returns a lazily evaluated IEnumerable<T> containing every match. Find is an instance method on List<T> that eagerly returns the first match, or the default value for T if none is found. Choosing between them depends on whether you need all matches or only the first one, and on the collection type you are using.

What Each Method Does

The Where method is a LINQ extension available on any IEnumerable<T>. It returns an IEnumerable<T> that yields all elements matching the predicate. Because it is lazily evaluated, the result is not materialized until you iterate over it.

On the other hand, Find is an instance method defined on List<T>. It returns the first element that matches the predicate, or the default value for T if no match is found. Find is eagerly evaluated—it searches the list until it finds a match or reaches the end.

Consider the following simple example with a list of integers:

var numbers = new List<int> { 1, 2, 3, 4, 5 }; // Where: returns an IEnumerable that yields all even numbers var evenNumbers = numbers.Where(n => n % 2 == 0); // Find: returns the first even number, which is 2 int firstEven = numbers.Find(n => n % 2 == 0);

This distinction is fundamental. You use Where when you need all matches, and Find when you only care about the first one.

Return Type and Deferred Execution

The return type of Where is IEnumerable<T>, which supports deferred execution. This means the predicate is not evaluated until you enumerate the result. If you call ToList() on the result, the enumeration happens immediately, and you get a List<T> containing all matches. If you only iterate once, the lazy evaluation may avoid an extra copy, but it also means that if the source collection changes between the call to Where and the enumeration, you might see different results.

Find returns a single T value directly. There is no deferred execution; the search is performed immediately when you call Find. This gives you predictable behavior: the result is either the first match or the default value.

For example, if you modify the list after calling Where but before iterating, the result can differ:

var list = new List<int> { 1, 2, 3 }; var filtered = list.Where(n => n > 1); // lazy list.Add(4); Console.WriteLine(filtered.Count()); // 3, because 2,3,4 match int found = list.Find(n => n > 1); // eagerly returns 2 immediately list.Add(5); // found is still 2; no re-evaluation

This behavior matters when you are dealing with a collection that might be modified between a query and its consumption.

Using Find on Arrays and Other Collections

A common mistake is trying to call Find on an array. Because Find is a method of List<T>, it is not available on arrays or other IEnumerable<T> types directly. For arrays, you can use Array.Find, which behaves similarly and returns the first match. Here is an example:

int[] numbersArray = { 10, 20, 30, 40 }; int firstLarge = Array.Find(numbersArray, n => n > 25); // returns 30

If you only have an IEnumerable<T> and need the first match, Where combined with FirstOrDefault is the standard LINQ approach:

IEnumerable<int> sequence = GetNumbers(); int firstMatch = sequence.Where(n => n > 10).FirstOrDefault();

This is functionally similar to Find but works on any sequence and does not require a specific concrete type.

Performance Considerations

Both approaches have O(n) worst-case cost. The practical difference usually comes from whether the code scans the whole collection and from allocations. In particular:

  • Find stops at the first match. If matches are early in a large collection, Find avoids scanning the rest of the list.
  • Where(...).FirstOrDefault() also stops early because Where is lazy, so in that form the scanning behavior is nearly the same as Find.
  • Calling ToList() on a Where result forces enumeration of the entire source and allocates a new list. That is wasteful if you only need one element.

For a small list, either choice is normally fine. The difference becomes more relevant when the collection is large and the match is near the beginning.

The following example illustrates the unnecessary cost of materializing all matches when you only need one:

var bigList = Enumerable.Range(0, 1_000_000).ToList(); // This calls ToList() and copies all matches into a new list, then takes the first var first = bigList.Where(i => i > 500_000).ToList().First(); // This stops at the first match without building a full list var firstEfficient = bigList.Find(i => i > 500_000);

Avoid materializing a full list when you only need the first match unless you also need the rest of the matches later.

Handling No Matches

When no element satisfies the predicate, Find returns the default value for T. For reference types, that is null; for value types, such as int or struct, it is the zero-initialized value. This can be misleading if the default value is also a valid match.

For example, if you are searching for a user with a specific ID and the list contains no such user, Find returns null. That is easy to check. But if you are searching for an integer that might legitimately be 0, you cannot tell whether 0 is a match or the default value. In such cases, use FindIndex to get the index and check its value, or use TryGetValue-style logic if available.

Where itself does not have this issue because it returns an empty sequence when there are no matches. You can check Any() on the result to see if there was a match, but that forces evaluation of the predicate at least once. If you consume the result with FirstOrDefault() to get the first match, the same default-value caveat applies.

Here is a practical example of the default-value pitfall:

var ids = new List<int> { 5, 10, 15 }; int result = ids.Find(id => id == 0); // returns 0, but no match exists Console.WriteLine(result); // prints 0; you don't know if it matched

In this case, FindIndex is a better choice if you must distinguish between a match and a missing match.

Maintainability and Readability

Beyond performance, choosing between Where and Find affects code readability. If you need the first match, using Find directly communicates that intent clearly. If you write Where(...).FirstOrDefault(), the reader must understand the LINQ pipeline and the lazy behavior. Both are fine, but Find is more concise for lists.

However, Where is more general and works with any IEnumerable<T>, including arrays, IQueryable<T>, and custom sequences. If you later switch the underlying collection type from a List<T> to an array or an IEnumerable<T>, code that uses Find will break unless you also change to Array.Find or LINQ. Code that uses Where remains valid.

Consider this scenario: you wrote a method that accepts a List<T> and uses Find. Later, you change the parameter type to IReadOnlyList<T> to improve the API. Now Find is no longer available because IReadOnlyList<T> does not define it. You would need to rewrite the internal logic. If you had used Where from the start, you would not have this problem.

Choosing Between Where and Find

Use Find when:

  • The collection is a List<T> or an array (with Array.Find).
  • You need exactly the first element that matches a predicate.
  • You know that the collection is not going to change between the call and the use of the result.
  • You want eager, immediately executed behavior.

Use Where when:

  • You need all matching elements, not just the first.
  • The source is any IEnumerable<T>, including LINQ queries that might be lazily composed.
  • You might change the collection type in the future.
  • You want to combine multiple LINQ operations, such as ordering, grouping, or projecting later.

In many codebases, you will see both used appropriately. The key is to understand the semantics of each and to make a deliberate choice based on requirements.

Combining Find with Indexes and Other List Methods

When you need the index of the first match, FindIndex is directly useful. It returns the zero-based index of the first match or -1 if no match is found. This is particularly handy when you need to remove an item from a list after locating it without creating a new list.

List<string> fruits = new List<string> { "apple", "banana", "cherry" }; int index = fruits.FindIndex(f => f.StartsWith("b")); if (index >= 0) { fruits.RemoveAt(index); }

Using Find followed by Remove would first scan the list with the predicate, and then Remove would scan the list again using the default equality comparer. That is two scans, and the item removed is not necessarily the same item the predicate selected if the predicate is based on something other than default equality. FindIndex avoids that problem by giving you the index directly so you can call RemoveAt.

The Impact of Custom Equality and Mutable Defaults

Because Find returns the default value when no match is found, be cautious with mutable reference types. If the default is null, you likely handle that correctly. But if you are using a struct, the zero-initialized default might have no meaningful data. Always check whether the returned value is the default before using it as a successful match.

Similarly, Where itself does not have this issue: an empty sequence clearly indicates no matches. If you use Where(...).FirstOrDefault(), the same default-value caveat applies.

In summary, the choice between Where and Find hinges on whether you need all matches or only the first one, the concrete type of the collection, and whether you value early execution or lazy composition. By matching the method to the actual need, you keep the code clear and efficient.

C# Where vs Find: Which One Should You Use? | RYUSLOG DEV