1. Introduction
Imagine you have a coffee machine. Usually it just brews coffee and doesn't bother anyone, but sometimes the boss says: "Can you, please, after brewing the coffee shout 'Done!'?" — and here you need a configuration. We don't rewrite the coffee machine itself. We just create a function that it will execute at the end.
In C# this role is played by delegates — they let you pass chunks of code (methods, lambdas or anonymous methods) into methods so that the method can call them at the right moment. Roughly speaking, a delegate is a type that can hold references to methods with a specific signature.
Delegate definition
In C# a delegate is defined with the delegate keyword. Example:
// Delegate that refers to methods taking an int and returning a bool
public delegate bool PredicateInt(int x);
Now a variable of type PredicateInt can refer to any method (or lambda!) that takes one int and returns a bool.
What are delegates for?
- Passing logic as an argument (for example, for sorting, filtering, handling events)
- Subscribing to events (we'll talk about that later)
- Implementing callbacks
- Flexible APIs where part of the behavior is defined by the caller
Simple visual scheme
| Delegate type | Signature | Example invocation |
|---|---|---|
|
|
|
|
|
|
|
|
|
2. How a lambda turns into a delegate
Syntax
When you write a lambda expression, for example x => x > 5, you are basically creating a delegate object. A lambda doesn't "live" in a vacuum: it needs a type (someone must know its parameter list and return type). That's why a lambda expression in C# is always implicitly (or explicitly) converted into a delegate.
Example 1: Binding a delegate to a method
// Explicitly define a delegate
public delegate bool MyPredicate(int number);
class Program
{
static void Main()
{
// Assign a lambda to a variable of type MyPredicate
MyPredicate isEven = x => x % 2 == 0;
Console.WriteLine(isEven(4)); // true
Console.WriteLine(isEven(7)); // false
}
}
Example 2: Using standard delegates
C# includes a set of standard generic delegates: Action, Func<>, Predicate<>. They are used almost everywhere you write lambdas in code.
// Use Func<int, bool>
Func<int, bool> isPositive = number => number > 0;
Console.WriteLine(isPositive(-5)); // false
3. Standard delegates: Func, Action, Predicate
Func<...>
Used for methods that take something and return something.
Signature:
— The last type is the return value, the ones before it are parameter types, for example:
Func<int, string> — takes an int, returns a string
Func<int, string> intToString = number => "Number: " + number;
Console.WriteLine(intToString(7)); // "Number: 7"
Action<...>
Used when you need to do something but don't need to return anything (void).
Action<string> printHello = name => Console.WriteLine("Hi, " + name + "!");
printHello("John"); // "Hi, John!"
Predicate<T>
Basically a shorthand for Func<T, bool>. Used when you need a boolean check on an object (true/false).
Predicate<int> isOdd = x => x % 2 != 0;
Console.WriteLine(isOdd(3)); // true
Visual cheat-sheet
| Delegate | Signature | Use |
|---|---|---|
|
|
Transformation, projection |
|
|
Side-effects, output |
|
|
Filtering, searching |
Which delegate type to choose for a lambda?
- If a return value is expected, pick Func<...>
- If the method returns nothing (void), use Action<...>
- If you need a condition check, use Predicate<T>
Example: Filtering a list
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
// Expects Predicate<int>
List<int> evenNumbers = numbers.FindAll(x => x % 2 == 0);
Console.WriteLine(string.Join(", ", evenNumbers)); // 2, 4, 6
4. Useful nuances
Lambda expressions and collection methods: what's under the hood?
When you call a collection method passing a lambda, for example:
var adults = users.Where(u => u.Age >= 18);
The Where method expects an argument of type Func<T, bool>. So your lambda u => u.Age >= 18 is turned by the compiler into a delegate object of that type.
Flowchart: how it works
Your lambda --> C# compiler --> Delegate object (Func<T, bool>)
(u => u.Age >= 18) [type known] (Ready to be invoked in Where())
More on typing: type inference
Usually the delegate type is inferred by the compiler automatically if it's clear from context. For example, for the method List<T>.Find a Predicate<T> is expected, and the compiler knows the parameter type from the method signature.
List<string> words = new List<string> { "one", "two", "three" };
var result = words.Find(word => word.Length == 5); // Find expects Predicate<string>
Console.WriteLine(result); // "three"
If the context is not clear, you'll need to help the compiler:
// Explicitly specify the type
Func<int, bool> check = x => x > 10;
Returning delegates: function factories
Sometimes methods can return delegates — i.e., create "function factories". This is handy for generating dynamic behavior.
// Function returning a delegate (a lambda)
Func<int, int> GetMultiplier(int factor)
{
return x => x * factor;
}
var times3 = GetMultiplier(3);
Console.WriteLine(times3(5)); // 15
This works because the lambda (x => x * factor) captures the variable factor from the outer context (closure) and is returned as an object of type Func<int, int>.
5. Errors and confusions with delegates and lambdas
Signature mismatch
The compiler won't let you assign a lambda to a delegate if the parameters or return type don't match.
Func<int, int> f = x => "You can't return a string!"; // Compilation error
Error trying to use a lambda without a delegate
You can't just write a lambda and try to invoke it without a type:
// This won't work - the compiler can't infer the type
// var myFunc = x => x * 2; // Error CS0815
// myFunc(10);
To make it work, you need to explicitly specify the type or provide a context:
Func<int, int> myFunc = x => x * 2;
Console.WriteLine(myFunc(10)); // 20
Confusion between Action, Func and Predicate
Sometimes you can accidentally pick the wrong delegate type, which causes a signature mismatch. Remember the simple rule: Func — when there's a result, Action — when there's no result (void), Predicate<T> — when you need a boolean answer (bool).
GO TO FULL VERSION