Fundamentals – Part 15

11/28/2025
4 minute read

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.

An error has occurred. This application may no longer respond until reloaded. Reload x