Learning Objectives
- Declare and use a custom delegate type.
- Use the standard
Action<>,Func<>andPredicate<>delegates. - Write expression-bodied and block-bodied lambdas with
=>. - Method group conversion — create a delegate from a method name
- Understand the basic event publish/subscribe model.
Key concepts
It is a concept that adds type safety to a function pointer. It can contain any method as long as the signature (parameters/return type) is the same.
delegate int BinaryOp(int a, int b);
BinaryOp add = (a, b) => a + b;
int r = add(3, 4); // 7
.NET+%EC%9D%B4+%EC%A0%9C%EA%B3%B5%ED%95%98%EB%8A%94+%EC%9D%BC%EB%B0%98+%EB%8D%B8%EB%A6%AC%EA%B2%8C%EC% 9D%B4%ED%8A%B8%EB%A7%8C+%EC%95%8C%EC%95%84%EB%8F%84+99%+%EC%B6%A9%EB%B6%84%ED%95%A9%EB%8B%88%EB%8B%A4.
| Standard type | meaning | Yes |
|---|---|---|
Action / Action<T> / Action<T1,T2> … |
No return value | Action<string> log = s => Console.WriteLine(s); |
Func<TResult> / Func<T,TResult> … |
Last is the return type | Func<int,int,int> add = (a,b) => a+b; |
Predicate<T> |
returns bool (1-argument) |
Predicate<int> pos = n => n > 0; |
Predicate<T>is effectively the same asFunc<T, bool>, but some method signatures in BCL requirePredicate<T>, such asArray.Find/List<T>.FindAll.
One-line anonymous function notation. If the body is long, you can create blocks with curly braces.
Func<int,int> square = n => n * n; //Expression body
Func<int,string> classify = n => //Block body
{
if (n > 0) return "positive number";
return n < 0 ? "Negative number" : "0";
};
If the signature is correct, you can fill the delegate with just the method name, without the lambda.
list.ForEach(Console.WriteLine); //Only method name in place of Action<string>!
The event keyword adds two constraints to the delegate.
- Like
obj.OnAlert = ...from the outside, cannot be completely replaced (only+=,-=are allowed) - Cannot be called from outside — Issued only inside the declared class
public event Action<string>? OnAlert;
OnAlert?.Invoke("Danger!"); //subscribers?. Safety call by
See as example
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)}");
//top-level statements file, the type declaration must come last.
delegate int BinaryOp(int a, int b);
Execution result
add(3, 4) = 7
mul(3, 4) = 12
Apply(10, 5, add) = 15
Apply(10, 5, mul) = 50
Memo: A delegate is a first-class citizen that can be used anywhere: variable, parameter, or return value.
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)}");
Execution result
Hi, Alice!
Hi, Bob!
add(2, 3) = 5
square(7) = 49
Negative number
Note: Action is “action only”, Func is “return value”. Even if the number of parameters increases, the same type is expanded as is.
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);
Execution result
First positive number: 1
Even numbers: -2, 0, 2
3First value greater than: 0
Note: If the value you are looking for is not found, default(T) is returned. The default of int is 0, so to distinguish it from the real 0, use FindIndex, etc.
List<string> names = ["Alice", "Bob", "Charlie"];
names.ForEach(n => Console.WriteLine(n)); //lambda
names.ForEach(Console.WriteLine); //method group (more concise)
Execution result
Alice
Bob
Charlie
Alice
Bob
Charlie
Length: 7
Note: Method groups are slightly faster than lambdas (cached in .NET 7) and more readable. However, the signature must match exactly.
internal sealed class Sensor
{
public event Action<string>? OnAlert;
public void CheckTemperature(int t)
{
if (t >= 100) OnAlert?.Invoke($"High temperature warning: {t} degrees");
}
}
Execution result
[Log] High temperature warning: 120 degrees
[Screen] !! High temperature warning: 120 degrees!!
Memo: If you attach multiple handlers with +=, they will all be called in the order of registration. If you are worried about memory leaks, turn it off with -= when you no longer use it.
Common Mistakes
- Declare the
delegatetype directly — in most cases,Action/Funcis sufficient. - Trying to overwrite
eventwith=from outside — compilation error. Only available for+=/-=. - Capture the external variable within the lambda, pass it asynchronously, and replace the original variable — You will see an unintended value called “Closure Capture”.
- There are no parameters like
Func<int>, but()is omitted — the call isf(), and the variable reference isf. - Substitute
Predicate<T>andFunc<T,bool>— Direct substitution is not possible due to different types. You can recreate it with Lambda.
Cleanup
- Delegate to treat a method like a value, lambda to create it on the fly
Action/Func/Predicatetakes precedence over directdelegatedeclarations- Shorten your code by one step with method group conversion
eventis the issue/subscription model — only+=/-=are allowed, issuance is only inside the class
Try it yourself
cd src/DelegateBasics && dotnet run
cd ../ActionFunc && dotnet run
cd ../Predicate && dotnet run
cd ../MethodGroup && dotnet run
cd ../EventBasics && dotnet run