Delegates
What they do, how they work, why you might (or might not) create your own. Delegates are one of those C# features that seem simple on the surface—“a type that represents a method”—yet they sit at the core of many powerful language features: events, LINQ, callbacks, async code, functional constructs, and modern library design patterns. In this post, we’ll break down what delegates actually are, how the compiler treats them, when you should write your own delegate types, and when you absolutely shouldn’t. We’ll also clarify the difference between the built-in delegate types: Action, Func, and Predicate.
What Exactly Is a Delegate?
A delegate is a type-safe representation of a method. Think of it as a “function variable”: a reference that points to executable code.
public delegate void Logger(string message);
void LogToConsole(string msg) => Console.WriteLine(msg);
Logger logger = LogToConsole;
logger("Hello!");
Behind the scenes, a delegate contains:
- A method pointer
- An optional target object (for instance methods)
- Metadata about input/output types
Because of that, a delegate guarantees type safety—you can only assign methods with the correct signature.
How Delegates Work Under the Hood
When you declare a delegate:
public delegate int MathOp(int a, int b);
The compiler generates a sealed class that:
- Inherits from System.MulticastDelegate
- Has a constructor that takes a target + method pointer
- Has an Invoke method that matches your delegate signature
- Contains metadata used by the runtime to figure out how to call the method
Conceptually, the generated type looks like this:
public sealed class MathOp : MulticastDelegate
{
public MathOp(object target, IntPtr method);
public int Invoke(int a, int b);
}
This is why delegates behave like objects—you can store them, pass them around, return them, and assign them like variables.
OK, But Why Do We Use Delegates?
Callbacks
You pass a function to another function so it can later “call you back.”
Encapsulating logic
Libraries can expose delegate-based hooks:
public void ProcessItems(IEnumerable<int> items, Func<int, bool> filter);
Events
Events are nothing more than special delegates with restricted access:
public event Action SomethingHappened;
LINQ
Every LINQ method expects a delegate behind the scenes:
Where(Func<T, bool> predicate)
Select(Func<T, TResult> selector)
OrderBy(Func<T, TKey> keySelector)
Should You Create Your Own Delegate Type?
Yes — when the meaning matters
If the delegate has a semantic purpose, naming it improves clarity.
public delegate bool AuthorizationRule(User user);
Yes — when the signature is long or confusing
A long Func<T1, T2, T3, T4, TResult> is unclear.
A delegate type makes the code easier to read.
public delegate Response ApiCallHandler(Request req, CancellationToken ct);
Yes — when designing public APIs or frameworks
Named delegates make them more self-documenting.
No — for simple or internal logic
Don’t define custom delegates for something as simple as:
public delegate void Something(string s);
You could have just used:
Action<string>
No — for LINQ or short-lived callbacks
Use the built-in ones unless clarity is seriously impacted.
No — when it becomes redundant noise
Creating a delegate for every callback encourages over-engineering.
Build in delegate types
Action – A delegate that returns void
- Up to 16 parameters
- No return value
Examples:
Action log = () => Console.WriteLine("Hi");
Action<string> error = msg => Console.Error.WriteLine(msg);
Action<int, int> add = (a, b) => Console.WriteLine(a + b);
Func<T1, T2, ..., TResult> – Returns a value
- Up to 16 parameters
- Last generic type is the return type
Examples:
Func<int, int, int> add = (a, b) => a + b;
Func<string> getTimestamp = () => DateTime.UtcNow.ToString();
Predicate<T> – A special case of Func<t, bool>
Predicate is simply:
bool Predicate<T>(T item)
Example:
Predicate<int> isEven = n => n % 2 == 0;
Used for:
- Searching (
List<T>.Find) - Filtering
- Conditional checks
Why does it exist if Func<T, bool> works?
→ Because naming matters. Predicate<T> clearly signals a yes/no test.
When to Choose What?
| Scenario | Use |
|---|---|
| Logging, effects | Action |
| Computing a result | Func<...> |
| Filtering items | Predicate |
| Public semantic delegate | Custom delegate |
| Complex multi-argument signature | Custom delegate |
| Internal, short-lived callback | Built-in types |
Final Thoughts
Delegates might look like syntactic sugar, but they’re incredibly powerful. They enable functional programming patterns, simplify APIs, and form the backbone of events and LINQ. Use built-in delegates for most things, but create your own when clarity and semantic meaning matter.


