1. Introduction
Delegate is a type that describes a function "signature": what parameters it takes, what it returns. It's like a passport for a function or a club pass for code: if the signature matches — come in, if not — sorry, you won't be allowed.
Example:
public delegate int Operation(int a, int b);
This delegate represents a function that takes two int and returns an int. This is how programming looked "before lambdas" (fresh, but not very convenient):
Operation add = delegate (int x, int y)
{
return x + y;
};
With lambdas the code became more concise:
Operation add = (x, y) => x + y;
Fun fact: In C# a lambda expression is just a way to create an "anonymous function" that the compiler automatically turns into an instance of a delegate of the right type. And that makes the relationship between them almost indistinguishable!
Built-in delegates: Func, Action, Predicate
To avoid creating endless custom delegates for every case, C# has "generic" function types:
- Func<T1, T2, ..., TResult> — a generic delegate that takes parameters (T1, T2, ...) and returns a value of type TResult.
- Action<T1, T2, ...> — almost the same, but returns nothing (void).
- Predicate<T> — a generic delegate that takes one parameter and returns bool (a favorite for filters).
| Delegate | Return type | Syntax example |
|---|---|---|
|
|
|
|
|
|
|
|
|
Example:
Func<int, int, int> add = (a, b) => a + b;
Action<string> show = s => Console.WriteLine(s);
Predicate<int> isEven = n => n % 2 == 0;
2. Lambda expressions "inside" a delegate
Passing a lambda to a method that expects a delegate
Any method that accepts a delegate can also "eat" a lambda, as long as its signature matches what's expected.
public static void CalculateAndShow(int a, int b, Func<int, int, int> operation)
{
int result = operation(a, b);
Console.WriteLine($"Result: {result}");
}
// Call with a lambda
CalculateAndShow(5, 7, (x, y) => x * y); // Result: 35
Example with Action
void ProcessString(string message, Action<string> processor)
{
processor(message);
}
// Use a lambda
ProcessString("Hello, world!", s => Console.WriteLine(s.ToUpper()));
Example with Predicate
List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
List<int> evenNumbers = numbers.FindAll(n => n % 2 == 0);
That's when you realize: why invent custom delegates if there's a proven "wheel" from Microsoft.
How delegates and lambdas work together
flowchart TD
A[Lambda expression] -->|Creates| B((Delegate))
B -->|Passed as argument| C[Method]
B -->|Stored| D[Property/Field/Collection]
C -->|Invokes| E[Lambda code]
3. Passing a lambda as a delegate in your own methods
If your own method uses delegates, lambdas make usage super flexible.
Example: our mini-calculator
// Define a method to work with operations
static int PerformOperation(int a, int b, Func<int, int, int> operation)
{
return operation(a, b);
}
// Apply different operations (lambdas in action!)
int sum = PerformOperation(3, 4, (x, y) => x + y);
int multiply = PerformOperation(3, 4, (x, y) => x * y);
int max = PerformOperation(3, 4, (x, y) => x > y ? x : y);
Console.WriteLine(sum); // 7
Console.WriteLine(multiply); // 12
Console.WriteLine(max); // 4
See how elegantly the logic changes, instead of three separate methods?
4. Lambdas + delegates in LINQ (and beyond)
In LINQ methods expect a "small logical function", i.e. a delegate. Almost all standard LINQ methods accept these types:
- Func<T, bool> for filtering (Where)
- Func<T, TResult> for transformations (Select)
- Func<T, TKey> for sorting (OrderBy)
List<string> names = new List<string> { "Anna", "Oleg", "Viktor", "Yana" };
// Get only names longer than 3 characters
var longNames = names.Where(name => name.Length > 3);
// Transform names to uppercase
var upperNames = names.Select(name => name.ToUpper());
LINQ is "lambdas on steroids" because anything you can pass as a delegate you can pass as a lambda!
5. Combining: storing and passing delegate-lambdas
List of functions
A lambda is not just a temporary anonymous method. You can store a set of lambdas in a collection and use them dynamically.
List<Func<int, int, int>> operations = new List<Func<int, int, int>>
{
(x, y) => x + y,
(x, y) => x - y,
(x, y) => x * y,
(x, y) => x / y
};
foreach (var op in operations)
{
Console.WriteLine(op(10, 2));
}
This pattern often appears in tests, handlers, and plugins.
Dictionary of delegates
And now, if you're a fan of plug-and-play architecture:
var mathFuncs = new Dictionary<string, Func<int, int, int>>
{
{ "plus", (x, y) => x + y },
{ "minus", (x, y) => x - y },
{ "pow", (x, y) => (int)Math.Pow(x, y) }
};
string command = "pow"; // Simulate user input
if (mathFuncs.TryGetValue(command, out var operation))
{
Console.WriteLine(operation(2, 5)); // 32
}
else
{
Console.WriteLine("Command not found");
}
6. Useful nuances
Lambda-delegates: returning a delegate from a method
You can return delegates (and therefore lambdas) from methods — it's basically a function factory!
Func<int, int> GetMultiplier(int factor)
{
// Return a lambda-delegate that multiplies by factor
return x => x * factor;
}
var triple = GetMultiplier(3);
Console.WriteLine(triple(5)); // 15
var quadruple = GetMultiplier(4);
Console.WriteLine(quadruple(5)); // 20
That's how a closure becomes a real tool, not a toy!
Composing functions: Combine, Delegate.Combine and multicast delegates
Sometimes you want one "invoke" to run multiple functions. For example, event handling in a GUI: click a button — multiple handlers respond.
Action and delegates allow you to "chain" multiple functions:
Action<string> pipeline = s => Console.WriteLine("Step 1: " + s);
pipeline += s => Console.WriteLine("Step 2: " + s.ToUpper());
pipeline += s => Console.WriteLine("Step 3: " + s.Length);
pipeline("test");
// Prints:
// Step 1: test
// Step 2: TEST
// Step 3: 4
Convenient for "pipeline" scenarios or event handling.
Important: For functions that return values, the result will be only from the last call ("the tail of the function"). For Action — all are executed, since void.
Using lambda-delegates as callback parameters
Classic scenario — "call me when you finish".
void DownloadFile(string url, Action<string> onFinish)
{
// Simulate downloading...
System.Threading.Thread.Sleep(500);// (Simulate long work, don't use in real GUI apps)
onFinish($"Download '{url}' finished!");
}
DownloadFile("http://example.com", msg => Console.WriteLine(msg));
In real apps (e.g. when working with APIs, DBs, files) — this is a standard pattern.
Higher-order function: example filter by predicate
Lambda expressions and delegates let you write generic functions that accept other functions as parameters. This is the basis of the "higher-order function" pattern.
List<int> Filter(List<int> source, Predicate<int> condition)
{
List<int> result = new List<int>();
foreach (var item in source)
if (condition(item)) result.Add(item);
return result;
}
// Use different lambdas:
var onlyPositive = Filter(new List<int> { -2, 0, 2, 7 }, x => x > 0);
var onlyEven = Filter(new List<int> { -2, 0, 2, 7 }, x => x % 2 == 0);
You can even create "lambda factories":
Func<int, Predicate<int>> GetRangeChecker(int min, int max) =>
x => x >= min && x <= max;
Predicate<int> inRange = GetRangeChecker(1, 10);
var filtered = Filter(new List<int> { 0, 5, 10, 15 }, inRange); // 5, 10
7. Common mistakes and nuances of combining
If parameter signatures don't match, the compiler will grumble (CS1660: "Cannot convert lambda expression").
A lambda can unexpectedly "remember" variables that later change (closure) — be careful with captured variables.
Multicast delegates with return values return only the result of the last handler — often a surprise for beginners.
In places where Expression<Func<int,bool>> is required, you can't substitute a statement-lambda — you need an expression-lambda.
GO TO FULL VERSION