Using C# List FindAll to Filter Collections
c# list findall: Learn how to use List<T>.FindAll in C# to filter collections with a predicate, including syntax, examples, and performance considerations.
The List<T>.FindAll method, often referred to as C# List FindAll, is a direct way to filter a list. It takes a predicate and returns a new List<T> containing only the elements that satisfy the condition. This article covers its syntax, usage, and the tradeoffs you should consider when choosing it over alternatives like LINQ's Where.
Using FindAll with a Predicate
The FindAll method is defined on List<T> and accepts a Predicate<T> delegate. The predicate is a method that takes an element and returns a bool. FindAll evaluates the predicate for each element and builds a new list from those that return true. The original list is not modified.
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; List<int> evenNumbers = numbers.FindAll(n => n % 2 == 0);
In this example, the lambda expression n => n % 2 == 0 is the predicate. The result is a new List<int> containing 2, 4, 6, 8, 10. The original numbers list remains unchanged.
Understanding the Predicate Parameter
The predicate can be a lambda, a named method, or an anonymous delegate. The type is Predicate<T>, which is a delegate that returns bool and takes one T parameter. This is equivalent to a Func<T, bool> but is specific to the .NET framework's older delegate style.
bool IsEven(int n) => n % 2 == 0; List<int> evens = numbers.FindAll(IsEven);
Here, IsEven is a method group converted to Predicate<int>. This is useful when the filtering logic is reused elsewhere.
Practical Example: Filtering a List of Objects
Consider a Product class with Name, Price, and InStock properties. To get all in-stock products under a certain price, you can use FindAll:
public class Product { public string Name { get; set; } public decimal Price { get; set; } public bool InStock { get; set; } } List<Product> products = GetProducts(); List<Product> affordableInStock = products.FindAll(p => p.InStock && p.Price < 50);
The predicate combines two conditions. This is a common pattern for filtering domain objects without writing a loop.
FindAll vs LINQ Where
LINQ's Where method is an extension method on IEnumerable<T>. It uses deferred execution and returns an IEnumerable<T>. When you call ToList(), it materializes the result into a new list. FindAll is a method on List<T> itself and returns a List<T> directly.
| Aspect | FindAll | LINQ Where + ToList |
|---|---|---|
| Return type | List<T> | IEnumerable<T> (then List<T>) |
| Execution | Immediate | Deferred until enumerated |
| Source type | List<T> only | Any IEnumerable<T> |
| Availability | .NET 2.0+ | .NET 3.5+ (LINQ) |
For a List<T>, both approaches produce the same filtered list. The choice often comes down to style and whether you need to chain other LINQ operations. If you are already using LINQ for other parts of the query, Where is natural. If you have a List<T> and want a simple one-off filter, FindAll is direct and readable.
Performance and Allocation Considerations
FindAll allocates a new list and copies matching elements into it. The time complexity is O(n) because it examines every element once. The allocation is unavoidable if you need a separate filtered list. If you want to avoid allocation entirely, you could filter in place by removing non-matching elements, but that is often less efficient due to shifting elements.
One subtle point: FindAll uses a List<T> internally to accumulate results. The capacity grows as needed, which may cause multiple allocations for large lists. If you know the approximate number of matches, you could use a manual loop with a pre-sized list, but for typical use this overhead is negligible. The key is that FindAll does not modify the original list, which is usually the desired behavior.
Edge Cases and Common Mistakes
- Null predicate: Passing
nulltoFindAllthrowsArgumentNullException. Always ensure the predicate is not null. - Empty list: If the source list is empty,
FindAllreturns an empty list, not null. - Modifying the list inside the predicate: The predicate should not add or remove elements from the source list while
FindAllis iterating. Doing so can cause undefined behavior or exceptions. The predicate should only read from the element passed to it. - Returning a new list: Remember that the result is a new list. If you need to update the original list, you must assign the result back, e.g.,
myList = myList.FindAll(...).
When to Use FindAll vs Other Filtering Approaches
Use FindAll when you have a List<T> and want a filtered copy without introducing LINQ. It is also slightly more discoverable for developers who are not familiar with extension methods. If you are working with IEnumerable<T> or need to compose with other LINQ operators, use Where. For very large lists where memory is a concern, consider streaming with Where and processing elements one at a time rather than materializing a new list. The decision depends on the source type, the need for a materialized result, and the surrounding code style.