C# Predicate Delegate: Usage and Examples
c# predicate delegate: Learn how to use the C# predicate delegate to filter collections, validate inputs, and simplify conditional logic with clear examples.
The Predicate<T> delegate in C# is a built-in delegate that represents a method that takes one parameter of type T and returns a bool. It is commonly used to evaluate a condition against an object, enabling methods like List<T>.FindAll and List<T>.Exists to filter or search collections without requiring custom loops. Understanding the c# predicate delegate is essential for writing concise, readable collection processing code.
What Is the Predicate Delegate Syntax?
The Predicate<T> delegate is defined in the System namespace as:
public delegate bool Predicate<in T>(T obj);
The in keyword indicates that the type parameter is contravariant, meaning a predicate that works on a base type can be used where a predicate on a derived type is expected. The delegate accepts a single argument and returns a Boolean value that represents the result of the condition.
Because it is a delegate, you can assign to it any method, lambda expression, or anonymous method that matches this signature. The delegate itself does not contain logic; it is a type-safe function pointer that allows methods to be passed as arguments.
Declaring a Predicate with a Named Method
A common way to use a predicate is to define a method that encapsulates the condition and then pass it to a collection method. For example, suppose you have a list of integers and you want to find all even numbers:
using System; using System.Collections.Generic; class Program { static bool IsEven(int number) { return number % 2 == 0; } static void Main()\n { List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 }; Predicate<int> evenPredicate = IsEven; List<int> evenNumbers = numbers.FindAll(evenPredicate); Console.WriteLine(string.Join(", ", evenNumbers)); // Output: 2, 4, } }
Here, the method group IsEven is implicitly converted to a Predicate<int> instance. The FindAll method iterates over the list and invokes the predicate for each element, collecting those for which the predicate returns true. This approach keeps the condition logic separate and reusable, which is useful when the same check is needed in multiple places.
Using Predicate with List<T> Methods
The List<T> class provides several methods that accept a Predicate<T> parameter. The the most commonly used are:
FindAll– returns a new list containing all elements that satisfy the predicate.Exists– returnstrueif at least one element satisfies the predicate.RemoveAll– removes all elements that satisfy the predicate and returns the number removed.Find– returns the first element that matches, or the default value ofTif none found.FindIndex– returns the zero-based index of the first match, or -1.
Each method uses the the predicate in a slightly different way, but the delegate signature remains the same. For instance, to check whether a list contains any negative number:
List<int> numbers = new List<int> { 1, -2, 3, -4 }; bool hasNegative = numbers.Exists(n => n < 0);
The lambda expression n => n < 0 is is a concise way to create a predicate without a named method. This is often the preferred style because it keeps the condition inline with the the operation, making the code more local and readable.
Using Lambda Expressions as Predicates
Lambda expressions are the most common way to supply a predicate in modern C#. They reduce boilerplate and allow you to express the condition directly at the call site. The previous example can be rewritten as:
List<int> evenNumbers = numbers.FindAll(n => n % 2 == 0);
The compiler infers the parameter type from the context, so you do not need to specify (int n). This works because the lambda is converted to a Predicate<int> delegate. The same lambda can also be assigned to a Predicate<int> variable if you need to reuse it:
Predicate<int> isEven = n => n % 2 == 0; bool anyEven = numbers.Exists(isEven);
Lambdas are especially useful when the condition is simple and unlikely to be reused elsewhere. However, if the same predicate is used in many places, extracting it into a named method or a static lambda field can improve maintainability and avoid duplication.
Predicate vs Func<T, bool> and LINQ Where
LINQ's Where method does not accept a Predicate<T>; it expects Func<T, bool>. This is a subtle but important distinction. While both delegate types have the same signature (take a T and return a bool), they are different types and are not implicitly convertible to each other. You cannot pass a Predicate<T> directly to Where without an explicit conversion.
Predicate<int> isEven = n => n % 2 == 0; var evenQuery = numbers.Where(isEven); // Compile error
To use a predicate with LINQ, you need to convert it to a Func<int, bool>:
var evenQuery = numbers.Where(n => isEven(n));
Or simply define the lambda directly in the Where call. In practice, you rarely need to convert between the two. Use Predicate<T> when working with List<T> methods that specifically require it,, and use Func<T, bool> when using LINQ or when you need a more general delegate that could return other types in other contexts. The Predicate<T> type is semantically tied to the idea of a condition, while Func<T, bool> is just one of many generic function shapes.
Combining Predicates and Reusability
Predicates can be combined using logical operators to build more complex conditions. Because a predicate is just a delegate, you can create a new predicate that invokes other predicates. For example:
Predicate<int> isEven = n => n % 2 == 0; Predicate<int> isPositive = n => n > 0; Prededicate<int> isEvenAndPositive = n => isEven(n) && isPositive(n);
This pattern is useful when you want to reuse simple conditions and compose them without duplicating logic. You can also pass predicates as parameters to your own methods, allowing callers to customize behavior. For instance, a method that processes a collection and applies a predicate to filter items before further processing:
void ProcessItems(List<int> items, Prededicate<int> filter) { var filtered = items.FindAll(filter); // ... additional processing }
This makes the method flexible and adheres to the open/closed principle, because new filtering behavior can be added without modifying the method itself.
Performance and Allocation Considerations
Using a predicate delegate adds a layer of indirection, but the overhead is negligible in most applications. The delegate invocation is a virtual call, which is slightly more expensive than a direct method call, but for collection operations that already involve iteration, the impact is usually not measurable.
One area to watch is lambda closures. If a lambda captures variables from the enclosing scope, the compiler generates a closure object that is allocated on the heap each time the lambda is created. In a hot path, this can cause unnecessary garbage collection pressure. For example:
int threshold = 10; var matches = numbers.FindAll(n => n > threshold); // captures threshold
If this code runs in a tight loop, a new closure is allocated per iteration. To avoid this, you can define the predicate as a static method or use a static lambda (if the captured value is constant). Alternatively, you can hoist the predicate creation outside the loop if the captured value does not change.
int threshold = 10; Predicate<int> isAboveThreshold = n => n > threshold; foreach (var list in manyLists) { var matches = list.FindAll(isAboveThreshold); }
This reuses the same delegate instance, reducing allocations. In most business applications, this level of optimization is unnecessary, but it is worth knowing when processing large datasets or in performance-sensitive libraries.
Common Mistakes and Edge Cases
A common mistake is assuming that a predicate will be called exactly once per element. In methods like FindAll, the predicate is called for each element until the end of the list. In Exists, it may short-circuit after the first match. This behavior is documented, but it can affect side effects. A predicate should be a pure function—it should not modify state or have side effects—because the number and order of calls are not guaranteed across different methods.
Another edge case is null handling. If the collection contains null elements and the predicate dereferences the argument, a NullReferenceException can occur. Always check for null inside the predicate if nulls are possible:
Predicate<string> isLong = s => s != null && s.Length > 10;
Finally, remember that Predicate<T> is a delegate, so it can be combined with other delegates using += or -= to form multicast delegates. However, for predicates, multicast behavior is rarely useful because the return value of a combined delegate is the result of the last method in the invocation list, which is rarely what you want. Avoid using multicast with predicates unless you have a specific need.
Understanding these details helps you use the c# predicate delegate effectively without surprising runtime behavior.