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
intparameters - an
intreturn 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: 50Here, 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) => expressionor
(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); // 10Passing 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:
Action- represents a method that returns voidFunc- represents a method that returns a valuePredicate- 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"); // 56.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(""); // trueMany .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, 25Here:
ProcessEachis a higher-order function.Action<T>is the delegate type.n => Console.WriteLine(n * n)is a lambda used as anAction<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 -> 16This 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); // 96Here 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
delegatekeyword, but often you use:Actionfor methods that returnvoidFuncfor methods that return a valuePredicate<T>for methods that returnbool
- 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/Predicateallow you to:- filter, transform, and process collections
- build flexible and reusable utilities
- implement patterns like strategies without many extra classes