DEV Community

gannaosma
gannaosma

Posted on

Understanding Delegates in C#: The Complete Beginner’s Guide

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);
    }
}

Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

And then call:

FilterEven(list);
Enter fullscreen mode Exit fullscreen mode

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);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)

Enter fullscreen mode Exit fullscreen mode

🧩 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);
}

Enter fullscreen mode Exit fullscreen mode

🔍 Explanation

  • MyDelegate defines the function signature (a method that takes an int and returns a bool).
  • IsPositive and IsEven Both match that signature, so they can be assigned to a MyDelegate.
  • The Filter method accepts a MyDelegate parameter, meaning it can run any filtering logic passed to it.
  • The actual function (IsPositive or IsEven) is called indirectly through the delegate inside Filter.

✅ Output

Positive numbers:
1
2
4
6
8
9

Even numbers:
2
4
6
8
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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;
});
Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

So the compiler:

  1. Creates a hidden method (_AnonymousMethod) that contains your logic.
  2. Automatically creates a MyDelegate object referencing that method.
  3. Passes it to the Filter method.

💡 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);

Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

So this:

delegate bool MyDelegate(int item);
Enter fullscreen mode Exit fullscreen mode

can be replaced with:

Predicate<int> myPredicate = IsPositive;
Enter fullscreen mode Exit fullscreen mode

Example:

bool IsPositive(int number) => number > 0;

List<int> numbers = new() { 1, -2, 3, -4, 5 };

// Using Predicate
numbers.FindAll(IsPositive).ForEach(Console.WriteLine);

Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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, ...) 
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)