C# Delegates

1. What is a delegate?

A delegate is a type-safe reference to a method.

You can think of a delegate as a variable that can hold a method.
It stores:

  • which method to call
  • and allows you to call it later, indirectly

Key points:

  • Delegates have a method signature (parameter types + return type).
  • Only methods with a compatible signature can be assigned to that delegate.
  • Delegates allow you to pass behavior (logic) as a parameter.

This is the basis for higher-order functions and many functional-style patterns in C#.


2. Defining your own delegate types

You can define your own delegate type with the delegate keyword.

Example: simple delegate definition

public delegate int IntOperation(int x, int y);

This declares a delegate type named IntOperation that represents methods with:

  • two int parameters
  • an int return value

Methods compatible with this delegate

public static class MathOperations
{
    public static int Add(int a, int b) => a + b;
    public static int Multiply(int a, int b) => a * b;
}

Both Add and Multiply have the same signature, so they match IntOperation.

Creating and using a delegate instance

class Program
{
    static void Main()
    {
        IntOperation op;
 
        op = MathOperations.Add;
        int sum = op(3, 4);         // calls Add(3, 4)
 
        op = MathOperations.Multiply;
        int product = op(3, 4);     // calls Multiply(3, 4)
    }
}

You can also use the Invoke method explicitly:

int sum2 = op.Invoke(5, 6);

But usually you just call it like a method: op(5, 6).


3. Delegates as parameters (higher-order functions)

A higher-order function is a function that:

  • takes one or more functions as parameters, or
  • returns a function

In C#, delegates are the way to do this.

Example: method that takes a delegate

public static int CalculateAndPrint(IntOperation operation, int x, int y)
{
    int result = operation(x, y);
    Console.WriteLine($"Result: {result}");
    return result;
}

Usage:

IntOperation addOp = MathOperations.Add;
IntOperation mulOp = MathOperations.Multiply;
 
CalculateAndPrint(addOp, 10, 5); // Result: 15
CalculateAndPrint(mulOp, 10, 5); // Result: 50

Here, CalculateAndPrint is a higher-order function because:

  • It receives a function (IntOperation) as a parameter.
  • It uses that function internally.

This is how you can pass different pieces of logic into a single reusable method.


4. Lambda expressions

Writing a separate named method for each piece of logic can become verbose.
Lambda expressions are a shorter way to write anonymous functions (functions without a name).

General idea

A lambda expression has the form:

(parameters) => expression

or

(parameters) => 
{
    // multiple statements
    return result;
}

Examples:

// one parameter, expression body
x => x * x
 
// two parameters, expression body
(x, y) => x + y
 
// with types (optional if compiler can infer)
(int x, int y) => x + y
 
// block body with statements
(int x, int y) =>
{
    int result = x * y;
    Console.WriteLine(result);
    return result;
};

Lambdas can be assigned to delegate variables or passed directly as delegate arguments.


5. Using lambdas with custom delegates

Using our IntOperation delegate again:

public delegate int IntOperation(int x, int y);

Assigning lambda expressions to delegates

IntOperation add = (x, y) => x + y;
IntOperation max = (x, y) => x > y ? x : y;
 
int result1 = add(3, 4); // 7
int result2 = max(10, 7); // 10

Passing lambdas as parameters

int r1 = CalculateAndPrint((x, y) => x - y, 10, 3);  // subtraction
int r2 = CalculateAndPrint((x, y) => x * x + y * y, 3, 4); // x² + y²

No extra named methods are needed — the logic is written inline where it is used.


6. Predefined delegate types: Action, Func, Predicate

In modern C#, you rarely define your own delegate types for simple patterns.
The .NET framework already provides generic delegate types for most uses:

  1. Action - represents a method that returns void
  2. Func - represents a method that returns a value
  3. Predicate - represents a method that returns bool

These types work very well with lambda expressions.

6.1 Action

Action is for methods with no return value.

Examples:

Action sayHello = () => Console.WriteLine("Hello!");
Action<string> print = message => Console.WriteLine(message);
Action<int, int> printSum = (a, b) => Console.WriteLine(a + b);

Usage:

sayHello();                 // "Hello!"
print("Hi there");          // "Hi there"
printSum(3, 4);             // "7"

6.2 Func

Func is for methods that return a value.
The last type parameter is the return type.

Examples:

Func<int> getRandomNumber = () => new Random().Next(1, 101);
Func<int, int, int> add = (a, b) => a + b;
Func<string, int> length = s => s.Length;

Usage:

int number = getRandomNumber();
int sum = add(3, 5);           // 8
int len = length("hello");     // 5

6.3 Predicate<T>

Predicate<T> is a special delegate type for checks that return bool.

Predicate<int> isEven = x => x % 2 == 0;
Predicate<string> isNullOrEmpty = s => string.IsNullOrEmpty(s);

Usage:

bool result1 = isEven(4);          // true
bool result2 = isNullOrEmpty("");  // true

Many .NET APIs use Predicate<T> or equivalent Func<T, bool>.


7. Delegates + lambdas + LINQ-style APIs

Many built-in .NET methods use delegates and are usually called with lambda expressions.

7.1 Example: filtering a list

var numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
 
// using Predicate<int>
IEnumerable<int> evens = numbers.Where(x => x % 2 == 0);
 
// similar LINQ style (using Func<int, bool>)
var evens2 = numbers.Where(x => x % 2 == 0).ToList();

Here, x => x % 2 == 0 is a lambda assigned to a Predicate<int> or Func<int, bool> (depending on the method).

7.2 Example: transforming a list

var words = new List<string> { "apple", "banana", "cherry" };
 
var lengths = words.Select(word => word.Length).ToList();
// lengths: { 5, 6, 6 }

Select takes a Func<TSource, TResult>, in this case Func<string, int>.


8. Putting it all together — practical examples

8.1 Reusable “process list” function (higher-order function)

using System;
using System.Collections.Generic;
 
public static class ListProcessor
{
    public static void ProcessEach<T>(
        IEnumerable<T> items,
        Action<T> action
    )
    {
        foreach (var item in items)
        {
            action(item);
        }
    }
}

Usage with lambdas:

var numbers = new List<int> { 1, 2, 3, 4, 5 };
 
ListProcessor.ProcessEach(numbers, n => Console.WriteLine(n * n));
// prints: 1, 4, 9, 16, 25

Here:

  • ProcessEach is a higher-order function.
  • Action<T> is the delegate type.
  • n => Console.WriteLine(n * n) is a lambda used as an Action<int>.

8.2 Simple filtering utility with Predicate<T>

public static class FilterUtil
{
    public static List<T> Filter<T>(
        IEnumerable<T> source,
        Predicate<T> predicate
    )
    {
        var result = new List<T>();
        foreach (var item in source)
        {
            if (predicate(item))
            {
                result.Add(item);
            }
        }
        return result;
    }
}

Usage:

var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
 
// keep only odd numbers
var odds = FilterUtil.Filter(numbers, x => x % 2 == 1);
 
// keep numbers greater than 5
var greaterThanFive = FilterUtil.Filter(numbers, x => x > 5);

Here:

  • Predicate<T> represents the check logic.
  • Lambdas define that logic inline.

8.3 Composing operations with Func

public static class MathPipeline
{
    public static int ApplyTwice(Func<int, int> func, int value)
    {
        return func(func(value));
    }
}

Usage:

Func<int, int> doubleIt = x => x * 2;
Func<int, int> square = x => x * x;
 
int result1 = MathPipeline.ApplyTwice(doubleIt, 5); // double twice: 5 -> 10 -> 20
int result2 = MathPipeline.ApplyTwice(square, 2);   // square twice: 2 -> 4 -> 16

This shows how you can pass behavior around and build flexible pipelines.


8.4 Strategy pattern with delegates instead of interfaces

public class PriceCalculator
{
    private readonly Func<decimal, decimal> _discountStrategy;
 
    public PriceCalculator(Func<decimal, decimal> discountStrategy)
    {
        _discountStrategy = discountStrategy;
    }
 
    public decimal CalculatePrice(decimal basePrice)
    {
        return _discountStrategy(basePrice);
    }
}

Usage:

// 10% discount
var calculator10 = new PriceCalculator(price => price * 0.9m);
 
// 20% discount if price >= 100, otherwise no discount
var calculatorConditional = new PriceCalculator(price =>
{
    if (price >= 100m)
        return price * 0.8m;
    return price;
});
 
decimal p1 = calculator10.CalculatePrice(50m);        // 45
decimal p2 = calculatorConditional.CalculatePrice(120m); // 96

Here we used:

  • Func<decimal, decimal> as a strategy
  • Lambdas to define different discount behaviors

9. Summary

  • A delegate is a type-safe reference to a method.
  • You can define custom delegates with the delegate keyword, but often you use:
    • Action for methods that return void
    • Func for methods that return a value
    • Predicate<T> for methods that return bool
  • Higher-order functions are methods that take delegates (or return them), allowing you to pass logic around.
  • Lambda expressions are a concise way to write anonymous methods that fit perfectly with delegates.
  • Delegates + lambdas + Action/Func/Predicate allow you to:
    • filter, transform, and process collections
    • build flexible and reusable utilities
    • implement patterns like strategies without many extra classes