Delegates and Lambdas

In C#, functions are values: you can store a method in a variable, pass it to another method, and return it as a result. The type of such a value is a delegate, and the most convenient way to write one is a lambda expression like x => x * 2. Delegates and lambdas power LINQ, events, callbacks and much of modern C#.

Func and Action

.NET provides ready-made generic delegate types, so you rarely need to declare your own:

  • Func<T1, …, TResult> — takes parameters and returns a value. The last type argument is the return type.
  • Action<T1, …> — takes parameters and returns nothing.
  • Predicate<T> — takes one value and returns bool (equivalent to Func<T, bool>).
C#
Func<int, int> square = x => x * x;
Func<double, double, double> hypot = (a, b) => Math.Sqrt(a * a + b * b);
Func<string> greeting = () => "Hello!";
Action<string> shout = msg => Console.WriteLine(msg.ToUpper());
Predicate<int> isNegative = n => n < 0;

Console.WriteLine(square(7));
Console.WriteLine(hypot(3, 4));
Console.WriteLine(greeting());
shout("delegates are values");
Console.WriteLine(isNegative(-5));
Output
49
5
Hello!
DELEGATES ARE VALUES
True

Lambda syntax

A lambda has parameters, an arrow =>, and a body. The body can be a single expression or a block with statements:

C#
Func<int, bool> isEven = n => n % 2 == 0;                 // one parameter: no parentheses needed
Func<int, int, int> add = (a, b) => a + b;                 // several parameters
Func<string, int, string> repeat = (string s, int n) => string.Concat(Enumerable.Repeat(s, n));   // explicit types
Func<int, string> classify = n =>                          // statement body
{
    if (n < 0) return "negative";
    return n == 0 ? "zero" : "positive";
};

Console.WriteLine(isEven(10));
Console.WriteLine(add(2, 3));
Console.WriteLine(repeat("ab", 3));
Console.WriteLine(classify(-4));

var multiply = (int a, int b) => a * b;    // natural type: Func<int, int, int>
var withDefault = (int step = 1) => step * 10;
Console.WriteLine(multiply(6, 7));
Console.WriteLine(withDefault());
Output
True
5
ababab
negative
42
10

Passing behaviour into methods

Accepting a delegate parameter lets callers decide part of what a method does. One method handles many situations:

C#
int[] numbers = [5, 12, 8, 21, 3, 16];

Console.WriteLine(string.Join(", ", Filter(numbers, n => n > 10)));
Console.WriteLine(string.Join(", ", Filter(numbers, n => n % 2 == 1)));
Console.WriteLine(string.Join(", ", Transform(numbers, n => n * 100)));
Console.WriteLine(string.Join(", ", Transform(numbers, n => $"<{n}>")));

static List<T> Filter<T>(IEnumerable<T> items, Func<T, bool> keep)
{
    var result = new List<T>();
    foreach (T item in items)
        if (keep(item)) result.Add(item);
    return result;
}

static List<TOut> Transform<TIn, TOut>(IEnumerable<TIn> items, Func<TIn, TOut> map)
{
    var result = new List<TOut>();
    foreach (TIn item in items) result.Add(map(item));
    return result;
}
Output
12, 21, 16
5, 21, 3
500, 1200, 800, 2100, 300, 1600
<5>, <12>, <8>, <21>, <3>, <16>

You've just written simplified versions of LINQ's Where and Select.

Method groups

Any method with a matching signature can be used as a delegate — just write its name without parentheses:

C#
string[] inputs = ["12", "abc", "7", "", "40"];

var valid = inputs.Where(IsNumber);
Console.WriteLine(string.Join(", ", valid));

int[] parsed = valid.Select(int.Parse).ToArray();
Console.WriteLine(parsed.Sum());

Array.ForEach(parsed, Console.WriteLine);

static bool IsNumber(string s) => int.TryParse(s, out _);
Output
12, 7, 40
59
12
7
40

Closures: capturing variables

A lambda can use variables from the method around it. It captures the variable itself, not a copy of its value, so later changes are visible — and the lambda can change it too:

C#
int counter = 0;
Action increment = () => counter++;

increment();
increment();
increment();
Console.WriteLine(counter);

Func<int, Func<int, int>> makeAdder = amount => x => x + amount;
var addTen = makeAdder(10);
var addHundred = makeAdder(100);
Console.WriteLine(addTen(5));
Console.WriteLine(addHundred(5));
Output
3
15
105

makeAdder returns a function. Each returned lambda remembers its own amount — that remembered environment is what "closure" means.

One trap: a classic for loop has a single variable shared by every pass, so lambdas created in the loop all see its final value. foreach creates a fresh variable each time and doesn't have this problem:

C#
var fromFor = new List<Func<int>>();
for (int i = 0; i < 3; i++)
    fromFor.Add(() => i);

var fromForeach = new List<Func<int>>();
foreach (int n in new[] { 0, 1, 2 })
    fromForeach.Add(() => n);

Console.WriteLine(string.Join(", ", fromFor.Select(f => f())));
Console.WriteLine(string.Join(", ", fromForeach.Select(f => f())));
Output
3, 3, 3
0, 1, 2

To fix the for version, copy the loop variable into a local inside the loop body: int copy = i; fromFor.Add(() => copy);. Mark a lambda static (static x => x * 2) to forbid capturing altogether.

Custom delegate types

You can declare your own delegate type with the delegate keyword. It gives the signature a meaningful name, which can make APIs clearer:

C#
PriceRule blackFriday = price => price * 0.7m;
PriceRule clearance = price => Math.Max(1m, price - 20m);

foreach (PriceRule rule in new[] { blackFriday, clearance })
{
    Console.WriteLine(rule(49.99m));
}

delegate decimal PriceRule(decimal original);
Output
34.993
29.99

Multicast delegates

A delegate can hold several methods. += adds one and -= removes one; invoking the delegate calls them all in order. Removing the last method leaves null, so the variable is declared nullable and called with ?.Invoke. This is the basis of events (next lesson):

C#
Action<string>? notify = msg => Console.WriteLine($"email: {msg}");
notify += msg => Console.WriteLine($"sms:   {msg}");
Action<string> log = msg => Console.WriteLine($"log:   {msg}");
notify += log;

notify?.Invoke("Order shipped");

notify -= log;
Console.WriteLine("--");
notify?.Invoke("Order delivered");
Output
email: Order shipped
sms:   Order shipped
log:   Order shipped
--
email: Order delivered
sms:   Order delivered

Simple lambda parameters with modifiers (C# 14)

When a delegate type has out or ref parameters, C# 14 lets a lambda add the modifier without spelling out every parameter type:

C#
TryParser<int> parse = (text, out result) => int.TryParse(text, out result);

if (parse("123", out int value))
    Console.WriteLine(value + 1);

delegate bool TryParser<T>(string text, out T result);
Output
124

Try it yourself

Build a small pipeline of text transformations as a list of Func<string, string> and apply them in order.

Try it yourself · Program.cs
Edit the code, copy it into Program.cs and run it with dotnet run Program.cs (.NET 10 or later). How to set up .NET
Output
[learning-delegates-a]