Skip to content

Intermediate · Exceptions, delegates, events and async · topic 27 of 43 · 30 min

Delegates, lambdas, Func, Action and Predicate

Explanation

A delegate is a variable that holds a method, so you can pass behaviour around like data. Instead of declaring your own delegate types, you usually use the built-in Func<...> (returns a value), Action<...> (returns nothing) and Predicate<T> (returns bool). A lambda such as x => x * 2 is a short way to write the method on the spot. Lambdas can capture variables from around them (closures).

Why pass methods around?

Sometimes the "what to do" changes but the "how to loop" stays the same. A sort needs to know how to compare. A filter needs to know which items to keep. A retry helper needs to know which operation to retry. Delegates let the caller pass that changing piece in as a parameter. LINQ's Where and Select work exactly this way.

Think of it like this: A delegate is like handing a courier a sealed instruction card: "when you reach the door, do this". The courier (the method) handles the journey; the card (the delegate) holds the action. Different customers hand different cards to the same courier.

A delegate type, the old way

csharp
PriceRule rule = Festive;           // store a method in a variable
Console.WriteLine(rule(1000m));     // 900.0
rule = NoChange;
Console.WriteLine(rule(1000m));     // 1000

static decimal Festive(decimal p) => p * 0.9m;
static decimal NoChange(decimal p) => p;

delegate decimal PriceRule(decimal price);   // type declarations go after top-level statements
A delegate type describes a method signature. Any method with that signature can be stored and called later.

Func, Action and Predicate

Built-in typeMeaningExample
Func<TResult>No inputs, returns TResultFunc<DateTime> now = () => DateTime.Now;
Func<T1, TResult>One input, returns a value (the LAST type is the return)Func<int, int> square = n => n * n;
Func<T1, T2, TResult>Two inputs, returns a valueFunc<decimal, decimal, decimal> add = (a, b) => a + b;
Action<T>Takes inputs, returns nothingAction<string> log = msg => Console.WriteLine(msg);
Predicate<T>Takes one input, returns boolPredicate<int> isEven = n => n % 2 == 0;

Lambda syntax

csharp
Func<int, int> twice = x => x * 2;                    // expression lambda
Func<int, int, int> add = (a, b) => a + b;            // two parameters
Func<string> hello = () => "Namaste";                 // no parameters
Func<decimal, decimal> withGst = amount =>            // statement lambda
{
    decimal gst = amount * 0.18m;
    return amount + gst;
};
var cube = (int n) => n * n * n;                      // C# 10: natural type, Func<int,int>

Console.WriteLine(twice(5) + add(2, 3));              // 15
Console.WriteLine(withGst(100m));                     // 118.00
Left of => are the parameters; right is the result (or a block with return).

Passing behaviour as a parameter

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

var marks = new List<int> { 35, 82, 47, 91, 28 };
var passed = Filter(marks, m => m >= 40);
Console.WriteLine(string.Join(", ", passed));   // 82, 47, 91
This is how Where works inside LINQ. The caller decides the rule; Filter does the looping.

Closures: lambdas remember variables

csharp
decimal rate = 0.05m;
Func<decimal, decimal> tax = amt => amt * rate;
rate = 0.12m;                       // the lambda sees the change
Console.WriteLine(tax(100m));       // 12.00
A lambda captures the variable itself, not a copy of its value at that moment. This is called a closure.
csharp
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
    actions.Add(() => Console.Write(i));
foreach (var a in actions) a();     // prints 333
All three lambdas captured the same i, which is 3 after the loop. Copy it inside the loop (int copy = i;) to capture each value. A foreach loop variable is fresh each round, so it does not have this problem.

Multicast delegates

csharp
Action<string> notify = msg => Console.WriteLine($"SMS: {msg}");
notify += msg => Console.WriteLine($"Email: {msg}");
notify("Order shipped");
A delegate can hold several methods; += adds one, -= removes one. Calling it calls them all in order. Events (next topic) are built on this.

Common mistakes and fixes

  • Mixing up Func and Action: if it returns something, use Func, and remember the LAST type parameter is the return type.
  • Calling a null delegate: NullReferenceException. Use handler?.Invoke(...).
  • Capturing a for-loop variable in lambdas: copy it to a local inside the loop.
  • Huge statement lambdas: if a lambda is longer than a few lines, make it a named method and pass the method name.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Delegates, lambdas, Func, Action and Predicate · 1/7

Delegates and lambdas

In this lesson you will pass methods around like data.

Prefer a video by a teacher? Find free videos on “C# delegates lambda expressions Func Action Predicate tutorial” on YouTube ↗

In real life: Pluggable discount rules for a festival sale

During Diwali a shopping site runs several discounts: 10% off electronics, Rs 100 off above Rs 1,000, and free shipping on certain items. The checkout keeps a list of rules as Func delegates and applies them in order, so marketing can add or remove rules without changing the checkout code.

csharp
var rules = new List<Func<decimal, decimal>>
{
    price => price >= 1000m ? price - 100m : price,
    price => Math.Round(price * 0.9m, 2),
};

decimal Apply(decimal price)
{
    foreach (var rule in rules) price = rule(price);
    return price;
}

Console.WriteLine(Apply(1500m));   // 1500 - 100 = 1400, then 10% off = 1260.0
Console.WriteLine(Apply(800m));    // no flat discount, 10% off = 720.0
Each rule is a Func<decimal, decimal>. Apply is a local function that uses the rules list.

Practice (0/4 done)

Task 1 · Question

Func<int, int, int> f = (a, b) => a * b + 1; What is f(3, 4)?

Task 2 · Question

Which type fits a method that takes a string and returns nothing?

Task 3 · Question

int x = 1; Func<int> get = () => x; x = 5; Console.WriteLine(get());

Task 4 · Practice

Write a method Retry(Func<bool> operation, int times, Action<int> onFail) that calls operation up to times times, stops when it returns true, and calls onFail(attemptNumber) after each failure. Test it with a lambda that fails twice and then succeeds (use a counter captured by the lambda).

Quick quiz

Question 1

1.In Func<string, int>, what is the return type?

Question 2

2.What is a closure?

Question 3

3.Predicate<int> p = n => n > 10; What is p(7)?

Question 4

4.Action a = () => Console.Write("X"); a += () => Console.Write("Y"); a(); prints...

Question 5

5.What is the safe way to call a delegate that might be null?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

In Func<string, int>, what is the return type?

Answer: int

The last type parameter of Func is the return type.

What is a closure?

Answer: A lambda that captures variables from the surrounding code

Captured variables live on as long as the lambda does.

Predicate<int> p = n => n > 10; What is p(7)?

Answer: false

7 > 10 is false.

Action a = () => Console.Write("X"); a += () => Console.Write("Y"); a(); prints...

Answer: XY

Multicast delegates call each method in the order added.

What is the safe way to call a delegate that might be null?

Answer: handler?.Invoke()

?.Invoke does nothing when the delegate is null.

Which call uses trailing lambda syntax?Related

Answer: list.filter { it > 0 }

The lambda moves outside the parentheses.

From Lambdas and higher-order functions

Inside func f(names ...string), what is the type of names?Related

Answer: []string

A variadic parameter is a slice inside the function.

From Variadic functions, function values and closures

When did lambdas arrive in Java?Related

Answer: Java 8

Java 8 (2014) added lambdas and streams.

From Lambdas and functional interfaces

What makes a lambda "generic"?Related

Answer: It uses auto in its parameter list

auto parameters turn the call operator into a template.

From Lambdas: small functions written in place

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 3 more topics in “Exceptions, delegates, events and async” before its exam: Events: letting objects notify each other, async and await: waiting without blocking, Files and JSON with System.Text.Json.
  2. Module 7 exam: Exceptions, delegates, events and async · 14 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Web APIs with ASP.NET Core and EF Core. 43 topics left in the course before the final exam.

← Previous: Exceptions: try, catch, finally and custom exceptions