Delegates
A delegate is a type-safe function pointer. It defines a contract or signature for methods (parameter types and return type), and it can hold references to one or more methods that match that signature. That lets a method be passed as an argument, or assigned to a variable.
Start simple
A delegate is a variable that holds a method, instead of a number or a string. If a method takes an int and returns a bool, a delegate can point to any method with that same shape. Here check is a variable, but instead of a value it stores a reference to the IsEven method. Calling check(4) runs IsEven(4).
delegate bool CheckNumber(int n); bool IsEven(int n) => n % 2 == 0; CheckNumber check = IsEven;Console.WriteLine(check(4)); // TrueWhat it’s for
The big use is that it lets you pass behavior into a method, not just data. Say you want to filter a list of numbers, but what “filter” means changes: sometimes the evens, sometimes the numbers over 10. Instead of a separate method for every case, write one method that takes a delegate. The same method then does something different each time, because you pass in what to do.
List<int> FilterNumbers(List<int> numbers, CheckNumber test){ List<int> result = new List<int>(); foreach (int n in numbers) if (test(n)) // calls whatever method was passed in result.Add(n); return result;} FilterNumbers(list, IsEven);FilterNumbers(list, n => n > 10); // even works with a lambdaThis is the core idea behind callbacks, events, LINQ and async code. It is built up one step at a time in from delegates to callbacks, events, LINQ and asyncFrom delegates to callbacks, events, LINQ and asyncCallbacks, events, LINQ and async/await are delegates with something added.Opens in a new tab.
In code
Here is one inside a small class. The delegate is declared on line 2, and it is also what the NumberChanged event on line 6 is typed with. Line 16 calls whatever methods the delegate is holding. The commented client code at the bottom hands it a lambdaLambda expressionsA short way to write a method inline: an anonymous function.Opens in a new tab on line 22.
// Define a delegate that takes an integer and returns voidpublic delegate void NumberChangedHandler(int newNumber); public class DataProcessor{ public event NumberChangedHandler NumberChanged; // Event using the delegate public void Process(int number) { //... processing logic... OnNumberChanged(number); // Raise the event } protected virtual void OnNumberChanged(int newNumber) { NumberChanged?.Invoke(newNumber); // Invoke the delegate if not null }} // Client code// DataProcessor processor = new DataProcessor();// processor.NumberChanged += (num) => Console.WriteLine($"Number changed to: {num}"); // Lambda subscribing// processor.Process(42);Built-in delegate types
| Type | Returns |
|---|---|
Action |
Nothing (void). E.g. Action, Action<int>. |
Func |
A value. The last type parameter is the return type. |
Predicate<T> |
Always bool. One input. |
Comparison<T> |
An int. Compares two items. |
EventHandler, EventHandler<TEventArgs> |
Used for events. |
Func<int>Func<int, bool>In both, the highlighted type is the return type.
EventHandler returns void. That is why an async event handler has to be async void, the one case where it is accepted. See why async void is dangerousWhy async void is dangerousIt discards the Task, so the caller cannot await it or catch its exceptions.Opens in a new tab. The e in its (object sender, EventArgs e) shape is covered in EventArgs and event dataEventArgs and event dataThe base class for the data an event carries.Opens in a new tab.
Here is one of each family:
Action doWork = () => Console.WriteLine("done");
Action<int> print = n => Console.WriteLine(n);
Action<int, int> add = (a, b) => Console.WriteLine(a + b);
// up to Action<T1..T16>
Func<int> getNumber = () => 42;
Func<int, bool> isEven = n => n % 2 == 0;
Func<int, int, int> sum = (a, b) => a + b;
// up to Func<T1..T16, TResult>
Predicate<int> isPositive = n => n > 0;
Comparison<int> compare = (a, b) => a.CompareTo(b);
list.Sort(compare);
Reach for Action and Func for almost everything now. Predicate<T> is mostly used in older APIs like List<T>.Find, and Func<T, bool> does the same job and is more common. Comparison<T> and custom delegates, like CheckNumber, mostly show up in older code, or in APIs that expect them.
Where it matters
Delegates are fundamental for event handlingEventsHow a class tells other classes that something happened, without knowing who is listening.Opens in a new tab, as the example above shows, and for implementing callback mechanisms. They are also the basis for LINQ expressions and for asynchronous programming patterns like the Task-based Asynchronous Pattern (TAP). They make the code more declarative and decoupled.
Delegates turn into callbacks, events, LINQ and async code step by step in this noteFrom delegates to callbacks, events, LINQ and asyncCallbacks, events, LINQ and async/await are delegates with something added.Opens in a new tab.
Sources
- Understanding Delegates in C#, ScholarHat · accessed May 16, 2025