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.
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.
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.
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.
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.
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.
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.
Func<int, int, int> add = (a, b) => a + b;
int result = add(10, 20);
Console.WriteLine(result);
Predicate Delegate
Predicate
Predicate<int> isEven = number => number % 2 == 0;
Console.WriteLine(isEven(10));
Console.WriteLine(isEven(7));
Action vs Func vs Predicate
| Delegate | Return Type | Typical Use |
|---|---|---|
| Action | void | Perform an operation |
| Func | Specified generic return type | Calculate or return a value |
| Predicate | bool | Check a condition |
Multicast Delegates
A delegate can reference multiple methods. When invoked, the methods are called in the order in which they were added.
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.
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.
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.
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.
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
| Approach | Purpose | Example |
|---|---|---|
| Named delegate | Define reusable method signature | delegate int Operation(int a, int b) |
| Lambda | Write delegate logic concisely | (a, b) => a + b |
| Action | Method returning void | Action |
| Func | Method returning a value | Func |
| Predicate | Boolean condition | Predicate |
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.