18. Delegate and Lambda
Delegates and lambdas are the tools that let you pass methods as values. Cover Action, Func, Predicate, lambda bodies, closures, and events.
What you'll learn
- 1Declare and use a `delegate` type yourself
- 2Pick between standard delegates `Action<>`/`Func<>`/`Predicate<>`
- 3Write lambda `=>` bodies in single-expression or block form
- 4Use **method group conversion** β fill a delegate with just a method name
- 5Understand the basic publish/subscribe model of `event`
Overview
In C#, **delegates (`delegate`) and lambdas** are the tools for treating methods themselves as values. LINQ, async, events, callbacks β almost every modern C# feature is built on these. This lecture lays that foundation.
Core Concepts
1) A delegate is "a type that points to a method"
It's a function pointer plus type safety. Any method matching the signature (parameters and return type) can be held.
delegate int BinaryOp(int a, int b);
BinaryOp add = (a, b) => a + b;
int r = add(3, 4); // 72) Standard delegates β don't roll your own
.NET's built-in generic delegates cover 99% of cases.
| Standard type | Meaning | Example |
|---|---|---|
| `Action` / `Action<T>` / `Action<T1,T2>` β¦ | **no return value** | `Action<string> log = s => Console.WriteLine(s);` |
| `Func<TResult>` / `Func<T,TResult>` β¦ | **last type is the return** | `Func<int,int,int> add = (a,b) => a+b;` |
| `Predicate<T>` | returns `bool` (1 arg) | `Predicate<int> pos = n => n > 0;` |
> `Predicate<T>` is effectively the same as `Func<T, bool>`, but some BCL methods like `Array.Find` / `List<T>.FindAll` specifically require `Predicate<T>`.
3) Lambda expression `=>`
Notation for an inline anonymous function. For a long body, use a brace block.
Func<int,int> square = n => n * n; // expression body
Func<int,string> classify = n => // block body
{
if (n > 0) return "positive";
return n < 0 ? "negative" : "0";
};4) Method group conversion
If the signature matches, you can fill a delegate with **just a method name, no lambda**.
list.ForEach(Console.WriteLine); // method name in place of an Action<string>!5) `event` β publish/subscribe model
The `event` keyword adds two restrictions to a delegate.
- From outside, you cannot **replace it wholesale** like `obj.OnAlert = ...` (only `+=`, `-=`)
- From outside, you cannot invoke it β **only the declaring class can publish**
public event Action<string>? OnAlert;
OnAlert?.Invoke("danger!"); // safe call with ?. for "no subscribers" caseExamples
Example 1 β `DelegateBasics`: declare a delegate type yourself
BinaryOp add = (a, b) => a + b;
BinaryOp mul = (a, b) => a * b;
int Apply(int x, int y, BinaryOp op) => op(x, y);
Console.WriteLine($"Apply(10, 5, add) = {Apply(10, 5, add)}");
Console.WriteLine($"Apply(10, 5, mul) = {Apply(10, 5, mul)}");
// In a top-level-statements file, type declarations come at the bottom.
delegate int BinaryOp(int a, int b);**Output**
add(3, 4) = 7
mul(3, 4) = 12
Apply(10, 5, add) = 15
Apply(10, 5, mul) = 50**Note:** Delegates are first-class β they can be variables, parameters, or return values.
Example 2 β `ActionFunc`: standard delegates
Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
Func<int, int, int> add = (a, b) => a + b;
Func<int, int> square = n => n * n;
greet("Alice");
Console.WriteLine($"add(2, 3) = {add(2, 3)}");
Console.WriteLine($"square(7) = {square(7)}");**Output**
Hello, Alice!
Hello, Bob!
add(2, 3) = 5
square(7) = 49
negative**Note:** `Action` is "action only", `Func` "returns a value." Add more parameters and the same type just extends.
Example 3 β `Predicate`: `Array.Find` / `FindAll`
Predicate<int> isPositive = n => n > 0;
Predicate<int> isEven = n => n % 2 == 0;
int[] numbers = [-3, -2, -1, 0, 1, 2, 3];
int firstPositive = Array.Find(numbers, isPositive);
List<int> evens = [.. numbers].FindAll(isEven);**Output**
First positive: 1
Evens: -2, 0, 2
First > 3: 0**Note:** If no element matches, `default(T)` is returned. `int`'s default is 0, so to distinguish from a real 0, use `FindIndex` etc.
Example 4 β `MethodGroup`: method group conversion
List<string> names = ["Alice", "Bob", "Charlie"];
names.ForEach(n => Console.WriteLine(n)); // lambda
names.ForEach(Console.WriteLine); // method group (cleaner)**Output**
Alice
Bob
Charlie
Alice
Bob
Charlie
Length: 7**Note:** Method groups are slightly faster (cached in .NET 7+) and easier to read β but signatures must match exactly.
Example 5 β `EventBasics`: `event` publish/subscribe
internal sealed class Sensor
{
public event Action<string>? OnAlert;
public void CheckTemperature(int t)
{
if (t >= 100) OnAlert?.Invoke($"high-temp alert: {t}Β°");
}
}**Output**
[log] high-temp alert: 120Β°
[screen] !! high-temp alert: 120Β° !!**Note:** Multiple handlers attached with `+=` run **in registration order**. To avoid memory leaks, `-=` to detach when no longer needed.
Full example code (src/)
src/ActionFunc/ActionFunc.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Modern18</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/ActionFunc/Program.cs
// Action<T...> : standard delegate for methods with no return value
Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
greet("Alice");
greet("Bob");
// Func<...,TResult> : last type is the return type
Func<int, int, int> add = (a, b) => a + b;
Func<int, int> square = n => n * n;
Console.WriteLine($"add(2, 3) = {add(2, 3)}");
Console.WriteLine($"square(7) = {square(7)}");
// Lambda body can be one line, or a brace block.
Func<int, string> classify = n =>
{
if (n > 0) return "positive";
if (n < 0) return "negative";
return "0";
};
Console.WriteLine(classify(-5));
src/DelegateBasics/DelegateBasics.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Modern18</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/DelegateBasics/Program.cs
// Declare a delegate type and use methods as variables.
BinaryOp add = (a, b) => a + b;
BinaryOp mul = (a, b) => a * b;
Console.WriteLine($"add(3, 4) = {add(3, 4)}");
Console.WriteLine($"mul(3, 4) = {mul(3, 4)}");
// Delegates can be stored in variables and passed to other methods.
int Apply(int x, int y, BinaryOp op) => op(x, y);
Console.WriteLine($"Apply(10, 5, add) = {Apply(10, 5, add)}");
Console.WriteLine($"Apply(10, 5, mul) = {Apply(10, 5, mul)}");
// In a top-level-statements file, type declarations come at the bottom.
delegate int BinaryOp(int a, int b);
src/EventBasics/EventBasics.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Modern18</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/EventBasics/Program.cs
// 'event' is a delegate member used to "publish notifications to many subscribers".
Sensor sensor = new();
// Subscribe: add handlers with +=.
sensor.OnAlert += msg => Console.WriteLine($"[log] {msg}");
sensor.OnAlert += msg => Console.WriteLine($"[screen] !! {msg} !!");
sensor.CheckTemperature(80); // normal
sensor.CheckTemperature(120); // publishes alert
internal sealed class Sensor
{
// event keyword: outside cannot replace via =, only += / -= allowed.
public event Action<string>? OnAlert;
public void CheckTemperature(int t)
{
if (t >= 100)
{
OnAlert?.Invoke($"high-temp alert: {t}Β°"); // safe call with ?. (no subscribers OK)
}
}
}
src/MethodGroup/MethodGroup.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Modern18</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/MethodGroup/Program.cs
// Instead of a lambda, hand over an existing method by name β "method group conversion".
List<string> names = ["Alice", "Bob", "Charlie"];
// Lambda version
names.ForEach(n => Console.WriteLine(n));
// Method-group version β method name alone if the signature matches
names.ForEach(Console.WriteLine);
// Func can also be filled by method group.
Func<string, int> length = GetLength;
Console.WriteLine($"Length: {length("Charlie")}");
static int GetLength(string s) => s.Length;
src/Predicate/Predicate.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Modern18</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/Predicate/Program.cs
// Predicate<T> : single-argument delegate returning bool (essentially Func<T, bool>)
Predicate<int> isPositive = n => n > 0;
Predicate<int> isEven = n => n % 2 == 0;
int[] numbers = [-3, -2, -1, 0, 1, 2, 3];
// Array.Find β first element matching the condition
int firstPositive = Array.Find(numbers, isPositive);
Console.WriteLine($"First positive: {firstPositive}");
// List<T>.FindAll β all matching elements as a new list
List<int> list = [.. numbers];
List<int> evens = list.FindAll(isEven);
Console.WriteLine($"Evens: {string.Join(", ", evens)}");
// You can pass a lambda inline.
Console.WriteLine($"First > 3: {Array.Find(numbers, n => n > 3)}"); // 0 if none (default)
Common Mistakes
- Declaring your own `delegate` type β `Action`/`Func` usually suffices.
- Trying to overwrite an `event` from outside with `=` β compile error. Only `+=` / `-=` are allowed.
- Capturing an outer variable in a lambda, then mutating the source before async runs it β "**closure capture**" leads to unexpected values.
- Forgetting `()` on a parameterless `Func<int>` β to call it use `f()`; bare `f` is the reference.
- Cross-assigning `Predicate<T>` and `Func<T,bool>` β **different types, no direct assignment**. Wrap in a lambda.
Summary
- To pass methods as values, use delegates; to build them inline, use lambdas
- Prefer standard `Action` / `Func` / `Predicate` over custom `delegate` declarations
- Method group conversion makes the code one step shorter
- `event` is publish/subscribe β only `+=`/`-=` allowed, publishing only inside the class
Practice
**Practice - 18. Delegate and Lambda**
Problem 1 β Number filter
- Project folder: `Homework01/`
- Key concepts: `Func<int,bool>` parameter, lambda, `IEnumerable<T>`
Requirements
- Write `IEnumerable<int> Filter(IEnumerable<int> source, Func<int,bool> predicate)`.
- For `int[] nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];` call it with lambdas to print:
- evens only
- β₯ 5
- primes (β₯ 2 with no divisors other than 1 and itself)
Expected output
Evens: 2 4 6 8 10
>= 5: 5 6 7 8 9 10
Primes: 2 3 5 7Hints
- Inside `Filter`, use `foreach` + `yield return`, or collect into a `List<int>`.
- Prime check: false if `n` < 2; try dividing from 2 up to `n-1` (simple version).
Problem 2 β Arithmetic calculator
- Project folder: `Homework02/`
- Key concepts: `Dictionary<string, Func<int,int,int>>`, registering lambdas, method groups
Requirements
- `Calculator` class holds `Dictionary<string, Func<int,int,int>> Ops` with 4 ops ("+", "-", "*", "/").
- `int Run(string op, int a, int b)` looks up the lambda and runs it.
- If the operator isn't registered, throw `InvalidOperationException`.
- Call and print:
- `Run("+", 10, 3)`, `Run("-", 10, 3)`, `Run("*", 10, 3)`, `Run("/", 10, 3)`
Expected output
10 + 3 = 13
10 - 3 = 7
10 * 3 = 30
10 / 3 = 3Hints
- Register with `Ops["+"] = (a, b) => a + b;` etc.
- Pattern: `if (!Ops.TryGetValue(op, out var fn)) throw new InvalidOperationException(...)`.
- Integer `/` returns the quotient only.
Check your answer
Try it yourself, then compare against the [`answer/`](./answer/) folder.
Answer (answer/)
homework/answer/Homework01/Homework01.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Modern18</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework01/Program.cs
int[] nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
Console.WriteLine("Evens: " + string.Join(" ", Filter(nums, n => n % 2 == 0)));
Console.WriteLine(">= 5: " + string.Join(" ", Filter(nums, n => n >= 5)));
Console.WriteLine("Primes: " + string.Join(" ", Filter(nums, IsPrime)));
// Takes Func<int,bool> and returns only matching elements.
static IEnumerable<int> Filter(IEnumerable<int> source, Func<int, bool> predicate)
{
foreach (int n in source)
{
if (predicate(n)) yield return n;
}
}
// Prime check β passed as a method group into Func<int,bool>.
static bool IsPrime(int n)
{
if (n < 2) return false;
for (int i = 2; i < n; i++)
{
if (n % i == 0) return false;
}
return true;
}
homework/answer/Homework02/Homework02.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Modern18</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework02/Program.cs
Calculator calc = new();
foreach (string op in new[] { "+", "-", "*", "/" })
{
int r = calc.Run(op, 10, 3);
Console.WriteLine($"10 {op} 3 = {r}");
}
internal sealed class Calculator
{
// Map op name β lambda.
public Dictionary<string, Func<int, int, int>> Ops { get; } = new()
{
["+"] = (a, b) => a + b,
["-"] = (a, b) => a - b,
["*"] = (a, b) => a * b,
["/"] = (a, b) => a / b,
};
public int Run(string op, int a, int b)
{
if (!Ops.TryGetValue(op, out Func<int, int, int>? fn))
{
throw new InvalidOperationException($"Unsupported operator: {op}");
}
return fn(a, b);
}
}
Try It Yourself
cd src/DelegateBasics && dotnet run
cd ../ActionFunc && dotnet run
cd ../Predicate && dotnet run
cd ../MethodGroup && dotnet run
cd ../EventBasics && dotnet runNext Lecture
[19_async_await](../19_async_await/) β See how lambdas and delegates shine in the async world.
All lecture materials and example code are openly available on GitHub.
View on GitHub β