← Back to C# series
✨
Modern C#
Modern C# Β· Prerequisite: methods/OOP

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.

C#.NET 8delegatelambda
Duration
⏱ ~1-1.5 hours
Level
πŸ“Š Intermediate
Prerequisite
🎯 OOP and methods completed
OUTCOME
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.

csharp
delegate int BinaryOp(int a, int b);

BinaryOp add = (a, b) => a + b;
int r = add(3, 4);   // 7

2) Standard delegates β€” don't roll your own

.NET's built-in generic delegates cover 99% of cases.

Standard typeMeaningExample
`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.

csharp
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**.

csharp
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**
csharp
public event Action<string>? OnAlert;
OnAlert?.Invoke("danger!");   // safe call with ?. for "no subscribers" case

Examples

Example 1 β€” `DelegateBasics`: declare a delegate type yourself

csharp
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**

text
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

csharp
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**

text
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`

csharp
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**

text
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

csharp
List<string> names = ["Alice", "Bob", "Charlie"];
names.ForEach(n => Console.WriteLine(n));   // lambda
names.ForEach(Console.WriteLine);           // method group (cleaner)

**Output**

text
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

csharp
internal sealed class Sensor
{
    public event Action<string>? OnAlert;
    public void CheckTemperature(int t)
    {
        if (t >= 100) OnAlert?.Invoke($"high-temp alert: {t}Β°");
    }
}

**Output**

text
[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

xml
<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

csharp
// 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

xml
<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

csharp
// 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

xml
<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

csharp
// '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

xml
<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

csharp
// 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

xml
<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

csharp
// 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

  1. Declaring your own `delegate` type β€” `Action`/`Func` usually suffices.
  2. Trying to overwrite an `event` from outside with `=` β€” compile error. Only `+=` / `-=` are allowed.
  3. Capturing an outer variable in a lambda, then mutating the source before async runs it β€” "**closure capture**" leads to unexpected values.
  4. Forgetting `()` on a parameterless `Func<int>` β€” to call it use `f()`; bare `f` is the reference.
  5. 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

text
Evens: 2 4 6 8 10
>= 5: 5 6 7 8 9 10
Primes: 2 3 5 7

Hints

  • 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

text
10 + 3 = 13
10 - 3 = 7
10 * 3 = 30
10 / 3 = 3

Hints

  • 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

xml
<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

csharp
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

xml
<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

csharp
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

bash
cd src/DelegateBasics && dotnet run
cd ../ActionFunc && dotnet run
cd ../Predicate && dotnet run
cd ../MethodGroup && dotnet run
cd ../EventBasics && dotnet run

Next Lecture

[19_async_await](../19_async_await/) β€” See how lambdas and delegates shine in the async world.

Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub β†—