Skip to content

C# - Delegates

A delegate in C# is a type that safely encapsulates a method, similar to a function pointer in C++. It acts as a reference to a method with a specific parameter list and return type. Delegates are the foundation for events, callbacks, and LINQ.

While you can declare custom delegate types, modern C# development heavily relies on the built-in generic delegates (Func, Action, Predicate) and lambda expressions for a more concise and powerful syntax.

The .NET library provides built-in generic delegate types that cover most common scenarios, removing the need to declare custom ones.

DelegateDescriptionExample
Func<T, TResult>Represents a method that takes one or more input parameters (T) and returns a value (TResult). There are overloads for up to 16 input parameters.Func<int, int, int> add; // Takes two ints, returns an int.
Action<T>Represents a method that takes one or more input parameters (T) but does not return a value (void).Action<string> print; // Takes a string, returns void.
Predicate<T>A specialized version of Func that takes one parameter (T) and always returns a bool. It is typically used for testing a condition.Predicate<int> isEven; // Takes an int, returns a bool.

Lambda Expressions: The Power of Conciseness

Section titled “Lambda Expressions: The Power of Conciseness”

A lambda expression is an anonymous function that you can use to create delegates or expression tree types. It provides a shorthand way to write methods without needing a full method declaration.

// Lambda Syntax: (input-parameters) => { expression-or-statement-block }
// Example 1: A Func<> that adds two numbers
Func<int, int, int> add = (x, y) => x + y;
int sum = add(10, 5);
Console.WriteLine($"Sum: {sum}"); // Output: Sum: 15
// Example 2: An Action<> that prints a message
Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
greet("World"); // Output: Hello, World!
// Example 3: A Predicate<> that checks if a number is positive
Predicate<int> isPositive = n => n > 0;
Console.WriteLine($"Is -5 positive? {isPositive(-5)}"); // Output: Is -5 positive? False

Delegates have a powerful feature called multicasting, where a single delegate can reference and invoke multiple methods. You can use the + and - (or += and -=) operators to add or remove methods from a delegate’s invocation list.

Action<string> logger;
Action<string> logToConsole = message => Console.WriteLine($"CONSOLE: {message}");
Action<string> logToFile = message => System.IO.File.AppendAllText("log.txt", $"{message}\n");
// Combine two methods into one delegate instance
logger = logToConsole;
logger += logToFile;
// Invoking 'logger' will now call both methods
logger("System startup complete.");
// Remove a method
logger -= logToConsole;
logger("User logged in."); // This will only be written to the file
Console.WriteLine("Logging complete. Check log.txt");

When you run this, “System startup complete.” is printed to the console and the log file. “User logged in.” is only written to the log file.

Delegates are the backbone of LINQ. Methods like Where, Select, and OrderBy all take delegates (usually as lambda expressions) to perform their operations.

var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
// 'Where' takes a Func<int, bool> (or Predicate<int>)
var evenNumbers = numbers.Where(n => n % 2 == 0);
// 'Select' takes a Func<int, TResult>
var squaredNumbers = evenNumbers.Select(n => n * n);
Console.WriteLine(string.Join(", ", squaredNumbers)); // Output: 4, 16, 36, 64, 100

Events in C# use delegates to define the signature for event handlers. When an event is raised, all registered handler methods are invoked.

public class Button
{
// The event is based on the Action delegate
public event Action? Clicked;
public void SimulateClick()
{
Console.WriteLine("Button was clicked!");
Clicked?.Invoke(); // Safely invoke the delegate
}
}
var myButton = new Button();
// Subscribe to the event using a lambda expression
myButton.Clicked += () => Console.WriteLine("Event handler: Responding to click!");
myButton.Clicked += () => Console.WriteLine("Another handler: Updating UI...");
myButton.SimulateClick();

When the above code is compiled and executed, it produces the following result:

Button was clicked!
Event handler: Responding to click!
Another handler: Updating UI...