Generic Delegates
Vaibhav • September 11, 2025
In the previous article, we explored covariance and contravariance-how generic interfaces and delegates can safely substitute types in input and output positions. Now we turn our focus to a concept that combines generics with event-driven and functional programming: generic delegates. Delegates are type-safe function pointers in C#, and when combined with generics, they become incredibly powerful tools for abstraction, composition, and reuse.
What Is a Generic Delegate?
A delegate defines the signature of a method that can be referenced and invoked later. A generic delegate allows you to define a delegate that works with any type, making it reusable across different contexts. You declare it using a type parameter just like generic classes or methods.
public delegate T Transformer<T>(T input);
This delegate represents any method that takes a value of type T
and returns a
value of the same type. You can use it for integers, strings, or even custom types.
Using Generic Delegates
Once you define a generic delegate, you can create instances of it by pointing to methods that match its signature. Let’s look at a simple example:
public static int Square(int x) => x * x;
Transformer<int> intTransformer = Square;
Console.WriteLine(intTransformer(5)); // Output: 25
Here, Transformer<int>
is bound to a method that squares an integer. The
delegate can now be invoked like a method.
Delegates are reference types. You can pass them as parameters, store them in collections, and invoke them dynamically.
Built-in Generic Delegates: Func, Action, Predicate
C# provides several built-in generic delegates that cover common patterns:
Func<T, TResult> represents a method that takes a parameter of type T
and returns a value of type TResult
.
Func<int, int> square = x => x * x;
Console.WriteLine(square(4)); // Output: 16
Action<T> represents a method that takes a parameter of type T
and returns void
.
Action<string> greet = name => Console.WriteLine($"Hello, {name}!");
greet("Vaibhav"); // Output: Hello, Vaibhav!
Predicate<T> represents a method that takes a parameter of type T
and returns a bool
.
Predicate<int> isEven = x => x % 2 == 0;
Console.WriteLine(isEven(6)); // Output: True
These delegates are used extensively in LINQ, event handling, and functional programming patterns.
Generic Delegates and LINQ
LINQ relies heavily on generic delegates. When you use methods like Where
,
Select
, or OrderBy
, you’re passing generic
delegates under the hood.
List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var evens = numbers.Where(x => x % 2 == 0);
The lambda x => x % 2 == 0
is compiled into a Func<int, bool>
delegate. This allows LINQ to filter the list based on your
condition.
Multicast Delegates with Generics
Delegates in C# can be multicast, meaning they can point to multiple methods. When invoked, all methods are called in order. This works with generic delegates too.
Action<string> notify = Console.WriteLine;
notify += msg => File.AppendAllText("log.txt", msg + "\n");
notify("Event triggered"); // Output to console and log file
This pattern is useful for logging, event broadcasting, and chaining behaviors.
Only the last method’s return value is preserved in a multicast delegate. If you need all results, use separate delegates or aggregate manually.
Generic Delegates and Events
Events in C# are built on delegates. You can use generic delegates to define flexible event handlers. For example:
public delegate void EventHandler<T>(T args);
public class Button
{
public event EventHandler<string> Clicked;
public void Click()
{
Clicked?.Invoke("Button was clicked");
}
}
This allows you to pass custom event data with strong typing. Subscribers can handle the event with methods that match the delegate signature.
Creating Delegate Chains with Generics
You can build chains of generic delegates to create pipelines or workflows. For example, a transformation pipeline:
Func<string, string> trim = s => s.Trim();
Func<string, string> toUpper = s => s.ToUpper();
Func<string, string> addExclamation = s => s + "!";
Func<string, string> pipeline = trim + toUpper + addExclamation;
string result = pipeline(" hello ");
Console.WriteLine(result); // Output: HELLO!
This pattern is useful for building data processors, validators, or formatters.
Generic Delegates and Type Inference
When using generic delegates, the compiler often infers the type parameters from the method or lambda you assign. This makes code cleaner and easier to read.
Transformer<int> doubleIt = x => x * 2;
Console.WriteLine(doubleIt(10)); // Output: 20
You don’t need to specify Transformer<int>
explicitly if the context
makes it clear.
Custom Generic Delegates vs Built-in
While built-in delegates like Func
, Action
,
and Predicate
cover most use cases, you may still want to define your own
generic delegates when:
- You need a more descriptive name for readability.
- You want to enforce a specific signature across your codebase.
- You’re building a framework or library with custom abstractions.
For example, defining a Validator<T>
delegate makes your intent clearer
than using Func<T, bool>
.
Common Pitfalls with Generic Delegates
While generic delegates are powerful, they can be misused:
- Avoid overly generic signatures that don’t convey intent.
- Don’t rely on multicast delegates for return values-only the last result is preserved.
- Be cautious with null delegates-always check before invoking.
if (myDelegate != null)
myDelegate(arg);
Use generic delegates to encapsulate reusable logic. Prefer built-in delegates for common patterns, and define custom ones when clarity or specialization is needed.
Summary
Generic delegates combine the flexibility of generics with the power of function pointers. They allow you to
define reusable, type-safe method references that work across types. You’ve learned how to define and use
generic delegates, how they integrate with LINQ, events, and pipelines, and how to leverage built-in delegates
like Func
, Action
, and Predicate
. You’ve also seen how to create custom delegates for clarity and
control. In the next article, we’ll explore the default keyword-how it interacts with generics,
what values it produces, and how to use it safely in your code.