Delegates in C
In C#, a delegate is a type-safe object that holds a reference to a method.
It allows you to pass methods as parameters, store them in variables, and call them dynamically, enabling flexible and reusable code.
🧩 Example Without Delegates
Let’s say we want to filter a list of integers based on different conditions.
In many programs, we perform similar operations that differ only by a specific rule — for example, filtering numbers based on various criteria.
internal class Program
{
static bool IsPositive(int item)
{
return item > 0;
}
static void FilterPositive(List<int> items)
{
foreach (int item in items)
{
if (IsPositive(item))
{
Console.WriteLine(item);
}
}
}
static void Main(string[] args)
{
List<int> list = new List<int>() { 1, 2, -3, 4, -5, 6, -7, 8, 9 };
FilterPositive(list);
}
}
This code prints all positive numbers from the list.
But what if we also want to filter even numbers or numbers greater than 5?
We’d have to create new methods like this:
static bool IsEven(int item)
{
return item % 2 == 0;
}
static void FilterEven(List<int> items)
{
foreach (int item in items)
{
if (IsEven(item))
{
Console.WriteLine(item);
}
}
}
And then call:
FilterEven(list);
We’d repeat this pattern for every new condition — duplicating code and making it harder to maintain.
💡 The Problem
All these methods (FilterPositive, FilterEven, etc.) do almost the same thing —
The only difference is the condition used inside the if statement.
So how can we make our code reusable and flexible, so that we can easily change the condition?
Imagine if we could pass a function as a parameter:
static void Filter(List<int> items, filterCondition)
{
foreach (int item in items)
{
if (filterCondition(item))
{
Console.WriteLine(item);
}
}
}
Here, filterCondition would represent any function that takes an integer and returns a boolean, allowing us to reuse the same filtering method for different conditions.
In other words, we need a way to pass behavior (a method) into another method — and that’s exactly what delegates are designed for.
🔍 What Are Delegates?
In C#, a delegate is a type that defines the signature of methods it can reference.
It acts like a “method pointer,” but is type-safe and object-oriented.
Every delegate type you declare in C# actually derives from System.MulticastDelegate, which in turn inherits from System.Delegate — the abstract base class that provides core delegate functionality.
You can’t create objects directly from Delegate, but when you declare a new delegate, the compiler automatically generates a class for it that inherits from System.Delegate.
Since not all functions have the same signature (parameters and return type), each delegate type must define its own.
This tells the compiler which kinds of methods the delegate can reference.
For example, if we want a delegate that refers to any function that takes an int and returns a boolWe declare it like this:
delegate bool MyDelegate(int item);
Behind the scenes, the compiler translates this into a new class (MyDelegate) that inherits from the abstract Delegate class.
We can then use MyDelegate as a type to reference any method that matches the same signature (int parameter, bool return type).
🧰 Creating and Using a Delegate Instance
Now that we’ve defined our delegate, MyDelegate becomes a compiler-generated class.
This class can be instantiated just like any other class.
Its constructor accepts a method reference — but only a method that matches the delegate’s signature.
Example:
MyDelegate d1 = new MyDelegate(IsPositive);
Here, d1 simply stores a reference to the IsPositive method.
It doesn’t execute it yet — it just “knows” which function to call.
To actually invoke the referenced method, the compiler provides a special method called Invoke(), automatically generated for every delegate type.
bool flag = d1.Invoke(5);
This calls the IsPositive function indirectly through the delegate.
However, you rarely call Invoke() manually.
C# lets you call the delegate as if it were a method — which is the preferred syntax:
bool flag2 = d1(6); // same as d1.Invoke(6)
🧩 Passing Delegates as Parameters
Now that we can create delegate instances, we can make our Filter method more dynamic by passing a delegate as a parameter.
This allows the same method to filter data using different conditions — without repeating similar code.
Example:
delegate bool MyDelegate(int item);
static bool IsPositive(int item)
{
return item > 0;
}
static bool IsEven(int item)
{
return item % 2 == 0;
}
static void Filter(List<int> items, MyDelegate myDelegate)
{
foreach (int item in items)
{
if (myDelegate(item))
{
Console.WriteLine(item);
}
}
}
static void Main(string[] args)
{
List<int> list = new List<int>()
{
1, 2, -3, 4, -5, 6, -7, 8, 9
};
MyDelegate d1 = new MyDelegate(IsPositive);
MyDelegate d2 = new MyDelegate(IsEven);
Console.WriteLine("Positive numbers:");
Filter(list, d1);
Console.WriteLine("\nEven numbers:");
Filter(list, d2);
}
🔍 Explanation
-
MyDelegatedefines the function signature (a method that takes anintand returns abool). -
IsPositiveandIsEvenBoth match that signature, so they can be assigned to aMyDelegate. - The
Filtermethod accepts aMyDelegateparameter, meaning it can run any filtering logic passed to it. - The actual function (
IsPositiveorIsEven) is called indirectly through the delegate insideFilter.
✅ Output
Positive numbers:
1
2
4
6
8
9
Even numbers:
2
4
6
8
This shows how delegates let you pass logic (methods) as parameters, making your code more reusable, flexible, and maintainable.
Now there’s another syntax sugar — instead of explicitly creating an object from MyDelegate, we can simply assign the function name directly, like this:
// MyDelegate d2 = new MyDelegate(IsEven);
MyDelegate d2 = IsEven;
This works because the compiler automatically creates a delegate object that refers to the IsEven method, as long as the method’s signature matches the delegate’s signature.
There’s also another syntax sugar:
If the only reason we’re creating d2 is to pass it to the Filter function, we can skip the variable entirely and pass the method directly:
// Filter(list, d2);
Filter(list, IsEven);
When we do this, the compiler checks if the method IsEven has the same signature as the delegate type expected by Filter.
If it does, the compiler automatically creates a delegate instance that refers to the IsEven function and passes it to Filter behind the scenes.
🧩 Using Anonymous Methods
Instead of defining a fully named function like IsPositive, you can use an anonymous function directly when calling Filter.
Filter(list, delegate (int item)
{
return item > 0;
});
Here, the delegate keyword allows you to define a method without a name — directly inline where it’s needed.
🧠 What Happens Behind the Scenes
When you write the above code, the C# compiler generates a private static method behind the scenes and assigns it to a delegate variable automatically.
Internally, it’s roughly equivalent to this:
private static bool _AnonymousMethod(int item)
{
return item > 0;
}
MyDelegate temp = new MyDelegate(_AnonymousMethod);
Filter(list, temp);
So the compiler:
- Creates a hidden method (
_AnonymousMethod) that contains your logic. - Automatically creates a
MyDelegateobject referencing that method. - Passes it to the
Filtermethod.
💡 In Short
| What You Write | What the Compiler Does |
|---|---|
Filter(list, IsPositive); |
Creates a delegate from an existing named method |
Filter(list, delegate(int item) { return item > 0; }); |
Creates a hidden method and a delegate object for it |
⚡ Then Came Lambda Expressions
C# later introduced lambda expressions, which make this syntax even shorter and more readable.
Because the compiler can infer that the delegate takes an int and returns a bool, you can simplify it to:
Filter(list, item => item > 0);
This lambda expression is just a more concise way to define the same anonymous method —
and the compiler still creates a hidden method and a delegate object behind the scenes.
⚙️ Built-in Delegate Types in C
When developers started using delegates heavily, everyone was declaring their own delegate types like:
delegate bool MyDelegate(int item);
That worked — but it became repetitive.
Most delegates just followed a few common patterns:
- Functions that return a bool
- Functions that return a value
- Functions that return nothing (void)
To solve this, .NET introduced three generic delegate types:
👉 Predicate<T>, Func<>, and Action<>.
🟢 1. Predicate<T>
A Predicate represents a method that takes one parameter and returns a bool.
👉 Signature:
Predicate<T> // equivalent to: delegate bool MyDelegate(T obj);
So this:
delegate bool MyDelegate(int item);
can be replaced with:
Predicate<int> myPredicate = IsPositive;
Example:
bool IsPositive(int number) => number > 0;
List<int> numbers = new() { 1, -2, 3, -4, 5 };
// Using Predicate
numbers.FindAll(IsPositive).ForEach(Console.WriteLine);
✅ Predicate<T> is commonly used in methods like:
List<T>.Find()List<T>.Exists()List<T>.RemoveAll()
🔵 2. Func<>
A Func represents any method that returns a value.
The last type parameter always represents the return type,
and all previous parameters represent the input arguments.
👉 Signature:
Func<TInput, TResult>
Examples:
| Example | Equivalent Delegate |
|---|---|
Func<int, bool> |
delegate bool MyDelegate(int item) |
Func<int, int, int> |
delegate int MyDelegate(int a, int b) |
Func<string> |
delegate string MyDelegate() |
Example usage:
Func<int, bool> isEven = x => x % 2 == 0;
Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(isEven(4)); // True
Console.WriteLine(add(3, 5)); // 8
✅ Func<> is the most flexible delegate type — it’s used almost everywhere in LINQ and modern C#.
🟠 3. Action<>
An Action represents a method that doesn’t return anything (void).
It can take 0 or more parameters, but always returns void.
👉 Signature:
Action<T1, T2, ...> // equivalent to: delegate void MyDelegate(T1 a, T2 b, ...)
Examples:
Action printHello = () => Console.WriteLine("Hello!");
Action<string> greet = name => Console.WriteLine($"Hi {name}!");
Action<int, int> printSum = (a, b) => Console.WriteLine(a + b);
printHello();
greet("Ganna");
printSum(3, 4);
✅ Action<> is often used for event handlers, callbacks, and performing operations without returning a result.
🧱 Built-in Delegates
To avoid writing custom delegate types every time, .NET provides three built-in generic delegate types:
| Delegate Type | Return Type | Parameters | Example |
|---|---|---|---|
| Action | void | 0–16 parameters | Action<int> print = n => Console.WriteLine(n); |
| Func | Any type | 0–16 parameters | Func<int, bool> isPositive = n => n > 0; |
| Predicate | bool | 1 parameter | Predicate<int> isEven = n => n % 2 == 0; |
🧾 Summary
| Concept | Description |
|---|---|
| Delegate | Type-safe reference to a method |
| Anonymous Method | Method defined inline without a name |
| Lambda Expression | Short, clean syntax for anonymous methods |
| Built-in Delegates | Generic types (Func, Action, Predicate) for reusability |
🧭 Conclusion
Delegates are one of the most powerful features in C#, enabling methods to be treated as first-class citizens.
They allow you to:
- Pass behavior as parameters
- Write flexible, reusable logic
- Decouple components for better maintainability
What started as a simple way to call methods indirectly evolved into anonymous methods, lambda expressions, and even LINQ — all built on top of delegates.
🚀 Next Steps
Now that you understand delegates, explore:
- Multicast Delegates — one delegate referencing multiple methods
- Events — higher-level abstraction built on delegates
- Expression Trees — used in LINQ for building dynamic queries
- Async Delegates — combining delegates with asynchronous programming
Top comments (0)