C# Delegates Explained: Methods as Variables and Callbacks

Delegates are an important C# feature that allows methods to be treated as values. They are commonly used for callbacks, events, LINQ operations, filtering, notifications, and flexible application design.

What is a Delegate in C#?

A delegate is a type-safe reference to a method. A delegate defines the method signature that compatible methods must follow.

CSHARP
delegate void MessageHandler(string message);

static void ShowMessage(string message)
{
    Console.WriteLine(message);
}

MessageHandler handler = ShowMessage;

handler("Hello from delegate!");

Why Use Delegates?

  • Pass methods as parameters
  • Implement callback functionality
  • Create flexible APIs
  • Build event-driven applications
  • Use lambda expressions
  • Support reusable filtering and transformation logic

Creating a Delegate

A delegate declaration specifies the return type and parameters of methods that can be assigned to it.

CSHARP
delegate int Calculator(int a, int b);

static int Add(int a, int b)
{
    return a + b;
}

Calculator calculator = Add;

int result = calculator(10, 20);

Console.WriteLine(result);

Passing a Delegate to a Method

Delegates become especially useful when a method needs to receive behavior from the caller.

CSHARP
delegate int Operation(int a, int b);

static int Calculate(int a, int b, Operation operation)
{
    return operation(a, b);
}

static int Add(int a, int b)
{
    return a + b;
}

int result = Calculate(10, 5, Add);

Console.WriteLine(result);

Delegates with Lambda Expressions

Lambda expressions provide a concise way to create delegate implementations without declaring a separate named method.

CSHARP
delegate int Operation(int a, int b);

Operation multiply = (a, b) => a * b;

int result = multiply(5, 4);

Console.WriteLine(result);

Anonymous Methods

C# also supports anonymous methods using the delegate keyword. Lambda expressions are generally preferred for concise modern code.

CSHARP
delegate void Printer(string text);

Printer printer = delegate(string text)
{
    Console.WriteLine(text);
};

printer("Hello");

Action Delegate

Action is a built-in generic delegate for methods that return void. It can accept zero or more parameters.

CSHARP
Action<string> print = message =>
{
    Console.WriteLine(message);
};

print("Hello World");

Func Delegate

Func is a built-in generic delegate used for methods that return a value. The final generic type represents the return type.

CSHARP
Func<int, int, int> add = (a, b) => a + b;

int result = add(10, 20);

Console.WriteLine(result);

Predicate Delegate

Predicate represents a method that accepts one value and returns a boolean. It is useful for conditions and filtering.

CSHARP
Predicate<int> isEven = number => number % 2 == 0;

Console.WriteLine(isEven(10));
Console.WriteLine(isEven(7));

Action vs Func vs Predicate

DelegateReturn TypeTypical Use
ActionvoidPerform an operation
FuncSpecified generic return typeCalculate or return a value
PredicateboolCheck a condition

Multicast Delegates

A delegate can reference multiple methods. When invoked, the methods are called in the order in which they were added.

CSHARP
Action message = () => Console.WriteLine("First");

message += () => Console.WriteLine("Second");
message += () => Console.WriteLine("Third");

message();

Removing Methods from a Multicast Delegate

Methods can be removed from a multicast delegate using the -= operator.

CSHARP
Action message = First;

message += Second;
message -= First;

message();

static void First()
{
    Console.WriteLine("First");
}

static void Second()
{
    Console.WriteLine("Second");
}

Delegates as Callbacks

A callback is a method that is supplied to another method so it can be executed later or after a particular operation completes.

CSHARP
static void ProcessData(Action<string> callback)
{
    string result = "Processing completed";

    callback(result);
}

ProcessData(message =>
{
    Console.WriteLine(message);
});

Delegates and LINQ

LINQ makes extensive use of delegates through Func and related types. Lambda expressions are commonly used to supply the required behavior.

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

var evenNumbers = numbers.Where(
    number => number % 2 == 0
);

foreach (int number in evenNumbers)
{
    Console.WriteLine(number);
}

Delegates and Events

Events are built on delegate types and provide a controlled mechanism for one object to notify other objects when something happens.

CSHARP
class Button
{
    public event Action? Clicked;

    public void Click()
    {
        Clicked?.Invoke();
    }
}

Button button = new Button();

button.Clicked += () =>
{
    Console.WriteLine("Button clicked");
};

button.Click();

Delegate Comparison

ApproachPurposeExample
Named delegateDefine reusable method signaturedelegate int Operation(int a, int b)
LambdaWrite delegate logic concisely(a, b) => a + b
ActionMethod returning voidAction
FuncMethod returning a valueFunc
PredicateBoolean conditionPredicate

Common Mistakes to Avoid

  • Creating custom delegates when Action or Func is sufficient
  • Using multicast delegates without considering exception behavior
  • Making delegate-based APIs unnecessarily complicated
  • Forgetting to check nullable event delegates before invocation
  • Using delegates when a simple direct method call would be clearer
  • Creating callbacks that make program flow difficult to understand

Delegate Best Practices

  • Use Action, Func, or Predicate when they clearly express the required signature
  • Use custom delegates when the delegate represents an important domain concept
  • Keep callback behavior simple and predictable
  • Use events when exposing notifications from a class
  • Prefer clear lambda expressions over unnecessarily verbose delegate syntax
  • Avoid excessive callback nesting

Real-World Applications

  • Event handling
  • LINQ queries
  • Callbacks
  • Validation logic
  • Sorting and filtering
  • Plugin-style behavior
  • Notification systems

Practice Exercises

  • Create a delegate for a calculator operation
  • Pass a delegate to a method
  • Rewrite a delegate using a lambda expression
  • Practice Action and Func
  • Create a Predicate for filtering numbers
  • Build a simple callback system
  • Create an event using a delegate

Conclusion

Delegates allow C# programs to treat methods as values and pass behavior between components. They form the foundation of callbacks, events, lambda expressions, and many features of LINQ and the .NET ecosystem.

Note: Note: Use delegates when passing behavior provides real flexibility. For simple operations, a normal method call is often easier to read and maintain.