π Lesson 2.1: Delegates and Lambdas
Until now, data was data and methods were methods. In this lesson those worlds merge: you'll learn to treat a method itself as a value you can store, pass to other methods, and call later. This idea powers events, LINQ, and modern C#.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Explain what a delegate is and why "methods as data" is useful
- Declare a delegate type, assign a method to it, and invoke it
- Use the built-in
Func,Action, andPredicatedelegates - Write concise lambda expressions with
=> - Pass behavior into a method as an argument
Estimated Time: 60 minutes
Project: Build a mini toolkit that takes behavior as a parameter.
In This Lesson
Methods as Data
You already pass values into methods β numbers, strings, objects. What if you could pass a method itself, so the receiving code can call it? That's exactly what delegates make possible.
π Definition
Delegate: a type that holds a reference to a method. A delegate variable can store any method whose signature (parameters and return type) matches, and you can invoke that method through the variable.
Think of a delegate as a "slot" for a method. The slot specifies the shape of method it accepts (say, "takes two ints, returns an int"), and you can plug in any matching method β then swap it out later.
(a slot for a method)"] --> B["Add()"] A -.->|"reassign"| C["Multiply()"] A --> D["Invoke β runs whichever
method is plugged in"] style A fill:#eff6ff,stroke:#3b82f6,stroke-width:2px style D fill:#e8f5e9,stroke:#4CAF50,stroke-width:2px
This unlocks a powerful pattern: writing general code that does the structure of a task, while the caller supplies the specific behavior.
Declaring a Delegate
You can declare a delegate type with the delegate keyword. It looks like a method signature with no body:
// A delegate type: "any method taking two ints and returning an int"
delegate int MathOp(int a, int b);
Now any matching method can be stored in a variable of that type and called through it:
int Add(int a, int b) => a + b;
int Multiply(int a, int b) => a * b;
MathOp op = Add; // plug in the Add method
Console.WriteLine(op(3, 4)); // 7 β calling Add through the delegate
op = Multiply; // swap in a different method
Console.WriteLine(op(3, 4)); // 12 β now it calls Multiply
π‘ Note the signatures match
Both Add and Multiply take two ints and return an int β exactly what MathOp requires. A method with a different signature won't fit the slot, and the compiler will say so. (Also note: you assign Add without parentheses β op = Add stores the method; op(3, 4) calls it.)
Func, Action, and Predicate
In practice you rarely declare your own delegate types, because .NET provides ready-made generic ones that cover almost every case. These are the ones you'll use constantly:
| Delegate | Represents a method that⦠|
|---|---|
Action | takes no arguments and returns nothing (void) |
Action<T> | takes a T and returns nothing |
Func<TResult> | takes nothing and returns a TResult |
Func<T, TResult> | takes a T and returns a TResult |
Predicate<T> | takes a T and returns a bool (a test) |
The rule for Func: the last type parameter is the return type; the earlier ones are parameters. So Func<int, int, int> means "takes two ints, returns an int" β our MathOp without the custom declaration:
Func<int, int, int> add = Add; // two int params, int result
Console.WriteLine(add(3, 4)); // 7
Action<string> greet = name => Console.WriteLine($"Hi, {name}!");
greet("Ada"); // Hi, Ada!
Predicate<int> isEven = n => n % 2 == 0;
Console.WriteLine(isEven(10)); // True
β Prefer the built-ins
Reach for Func, Action, and Predicate instead of writing your own delegate types. They're instantly recognizable to other developers and are what .NET's own APIs (including LINQ) expect.
Lambda Expressions
Writing a whole named method just to plug it into a delegate is often overkill. A lambda expression is a compact, inline, anonymous method. You've already seen the => ("goes to") arrow in expression-bodied methods; lambdas use the same arrow:
// Parameters => result
Func<int, int, int> add = (a, b) => a + b;
Func<int, int> square = x => x * x;
Action greet = () => Console.WriteLine("Hello!");
Console.WriteLine(add(3, 4)); // 7
Console.WriteLine(square(5)); // 25
greet(); // Hello!
Anatomy of a lambda:
- Parameters in parentheses on the left β
(a, b). With exactly one parameter you can drop the parentheses:x => x * x. - The
=>arrow. - The body on the right β a single expression whose value is returned automatically.
For multiple statements, use braces and an explicit return:
Func<int, string> describe = n =>
{
if (n > 0) return "positive";
if (n < 0) return "negative";
return "zero";
};
Console.WriteLine(describe(-5)); // negative
π‘ Types are inferred
You didn't declare types for a, b, or x β C# infers them from the delegate the lambda is assigned to. That brevity is the whole point: lambdas let you express behavior with minimal ceremony.
Passing Behavior to Methods
Here's where it all pays off. A method can accept a delegate as a parameter, letting the caller inject custom behavior. Consider a method that processes a list but lets you decide the operation:
// Applies the given operation to every number and prints the result
void ForEachNumber(List<int> numbers, Action<int> action)
{
foreach (int n in numbers)
{
action(n);
}
}
List<int> nums = new List<int> { 1, 2, 3 };
ForEachNumber(nums, n => Console.WriteLine(n * 10)); // 10, 20, 30
ForEachNumber(nums, n => Console.WriteLine($"[{n}]")); // [1], [2], [3]
Same loop, totally different behavior β chosen by the caller at the call site. Now a filtering example using a Func that returns a bool:
List<int> Filter(List<int> numbers, Func<int, bool> test)
{
List<int> results = new List<int>();
foreach (int n in numbers)
{
if (test(n)) // the caller decides what "keep" means
{
results.Add(n);
}
}
return results;
}
List<int> nums = new List<int> { 1, 2, 3, 4, 5, 6 };
List<int> evens = Filter(nums, n => n % 2 == 0); // 2, 4, 6
List<int> big = Filter(nums, n => n > 3); // 4, 5, 6
β This is how LINQ works
That Filter method is essentially LINQ's Where. In Lesson 2.3 you'll use the real, built-in versions β Where, Select, and friends β which all take lambdas exactly like this. Delegates and lambdas are the foundation LINQ is built on.
Capturing Variables
A lambda can use variables from the surrounding code, not just its own parameters. This is called capturing (or a "closure"):
int threshold = 3;
Func<int, bool> aboveThreshold = n => n > threshold; // captures 'threshold'
Console.WriteLine(aboveThreshold(5)); // True
Console.WriteLine(aboveThreshold(2)); // False
The lambda "remembers" threshold even though it wasn't passed in as a parameter. This makes lambdas great for building behavior tailored to the current context.
β οΈ Captured variables are live references
A lambda captures the variable, not a snapshot of its value. If the variable changes after the lambda is created, the lambda sees the new value:
int factor = 2;
Func<int, int> scale = n => n * factor;
Console.WriteLine(scale(10)); // 20
factor = 5; // change it afterward
Console.WriteLine(scale(10)); // 50 β uses the updated factor
This is usually what you want, but it can surprise you inside loops β a well-known gotcha worth remembering.
Exercise & Quiz
ποΈ Exercise: A Configurable Transformer
Objective: Write methods that take behavior as parameters, and call them with lambdas.
Instructions:
- Create a new project called
Behaviors. - Write a method
List<int> Transform(List<int> numbers, Func<int, int> op)that returns a new list withopapplied to each element. - Call it three ways with lambdas: double each number, square each number, and add 100 to each.
- Write a method
int CountWhere(List<int> numbers, Func<int, bool> test)that returns how many elements satisfy the test. Use it to count evens and count numbers greater than 50.
Starter Code:
List<int> nums = new List<int> { 1, 2, 3, 4, 5 };
// TODO: use Transform to double each, then print
List<int> Transform(List<int> numbers, Func<int, int> op)
{
// TODO: apply op to each element into a new list
return new List<int>();
}
// TODO: int CountWhere(List<int> numbers, Func<int, bool> test)
π‘ Hint
In Transform, loop and results.Add(op(n));. To double, call Transform(nums, n => n * 2). In CountWhere, keep a counter and if (test(n)) count++;.
β Solution
List<int> nums = new List<int> { 1, 2, 3, 4, 5 };
List<int> doubled = Transform(nums, n => n * 2);
Console.WriteLine(string.Join(", ", doubled)); // 2, 4, 6, 8, 10
List<int> squared = Transform(nums, n => n * n);
Console.WriteLine(string.Join(", ", squared)); // 1, 4, 9, 16, 25
Console.WriteLine(Transform(nums, n => n + 100)[0]); // 101
Console.WriteLine(CountWhere(nums, n => n % 2 == 0)); // 2 (evens: 2,4)
Console.WriteLine(CountWhere(nums, n => n > 50)); // 0
List<int> Transform(List<int> numbers, Func<int, int> op)
{
List<int> results = new List<int>();
foreach (int n in numbers)
{
results.Add(op(n));
}
return results;
}
int CountWhere(List<int> numbers, Func<int, bool> test)
{
int count = 0;
foreach (int n in numbers)
{
if (test(n))
{
count++;
}
}
return count;
}
(string.Join(", ", list) is a handy way to print a collection on one line.)
π― Quick Quiz
Question 1: What does a delegate hold?
Question 2: What does Func<int, bool> describe?
Question 3: In the lambda x => x * x, what is x?
Summary
π Key Takeaways
- A delegate is a type that references a method β a "slot" that accepts any method with a matching signature.
- Prefer the built-in generic delegates:
Action(returns void),Func<β¦, TResult>(returns a value, last type param is the result), andPredicate<T>(returns bool). - Lambdas (
parameters => body) are concise inline methods; single-expression bodies return automatically, and types are inferred. - Passing a delegate/lambda into a method lets the caller inject behavior β the pattern behind LINQ.
- Lambdas capture surrounding variables by reference (a closure), so later changes are visible.
π Additional Resources
- Delegates β C# Programming Guide
- Lambda expressions β reference
- Func<T,TResult> β API reference
π What's Next?
Delegates hold methods β and that's the mechanism behind events, C#'s way of letting objects broadcast notifications. In Lesson 2.2: Events, you'll build objects that others can subscribe to and react to.
π Behavior is now data!
You can pass logic around like any other value. Next, we use delegates to power events.