C# Generic Delegate: Syntax and Practical Usage
c# generic delegate: Learn how to declare and use generic delegates in C#, including Func, Action, and custom delegate types with compile-time type safety.
A C# generic delegate carries type parameters, allowing the same delegate definition to work with different data types without duplication. The most visible examples are the built-in Func<...>, Action<...>, and Predicate<T> types, but you can also declare your own generic delegate types when the built-in ones do not match the shape you need.
Declaring a Custom Generic Delegate
A delegate declaration follows the same syntax as a method signature, with type parameters placed in angle brackets:
public delegate T Transformer<T>(T input);
This declares a delegate named Transformer that accepts one argument of type T and returns a value of type T. The type parameter is resolved at the point of use, so the same delegate can handle int, string, or any other type:
Transformer<int> doubleValue = x => x * 2; Transformer<string> addExclamation = s => s + "!"; Console.WriteLine(doubleValue(21)); // 42 Console.WriteLine(addExclamation("hi")); // hi!
The compiler infers the delegate's signature from the lambda expression, but the type argument must be supplied explicitly when you declare the variable. The delegate instance stores both the method reference and the type information needed to invoke it safely.
Multiple Type Parameters and Variance
Generic delegates are not limited to a single type parameter. You can declare a delegate with several parameters and a return type:
public delegate TResult Converter<in TInput, out TResult>(TInput input);
The in and out modifiers control variance. in allows the delegate to accept a less derived type when assigned, and out allows it to return a more derived type. This matters when you pass delegates to methods that expect a base type but receive a derived type, or vice versa.
Converter<object, int> getLength = o => o.ToString().Length; Converter<string, int> stringLength = getLength; // valid: object delegate handles string Converter<string, string> identity = s => s; Converter<string, object> objectResult = identity; // valid: string delegate returns object
Without the in and out modifiers, both assignments fail at compile time. Variance in delegates is a compile-time feature; it does not change how the delegate behaves at runtime.
The Built-in Func, Action, and Predicate Types
Most code does not need a custom generic delegate because the framework provides three families that cover common signatures.
Func<TResult> and its overloads represent methods that return a value. The last type parameter is always the return type, and the preceding parameters are the input types:
Func<int, int, int> add = (a, b) => a + b; Func<string, int> parseLength = s => s.Length;
Action<T> represents methods that return void. The type parameters are all input types:
Action<string> log = message => Console.WriteLine(message); Action<int, int> report = (a, b) => Console.WriteLine($"{a} + {b}");
Predicate<T> represents a method that takes one argument and returns bool. It is used heavily by collection methods such as List<T>.FindAll and Array.Exists:
Predicate<int> isEven = n => n % 2 == 0; var numbers = new List<int> { 1, 2, 3, 4, 5 }; var evens = numbers.FindAll(isEven);
These built-in types cover the majority of delegate usage in modern C#. Custom generic delegates are usually reserved for cases where the signature does not fit Func, Action, or Predicate, or where a domain-specific name improves readability.
Generic Delegates as Method Parameters
A common pattern is to pass a generic delegate into a method so the caller can supply behavior. This is the foundation of many LINQ-style APIs:
public static IEnumerable<T> Filter<T>(IEnumerable<T> source, Predicate<T> predicate) { foreach (var item in source) { if (predicate(item)) { yield return item; } } }
The method is generic over T, and the delegate parameter uses the same type parameter. Callers can pass a lambda, a method group, or an existing delegate instance:
var result = Filter(numbers, isEven); var result2 = Filter(numbers, n => n > 3);
Method groups work because the compiler can convert a method with a matching signature to the delegate type. This keeps the API flexible without forcing callers to construct delegate instances explicitly.
Type inference works because numbers is List<int>, so the compiler knows T is int and can check the lambda against Predicate<int>. If the source argument carries no type information, such as null, inference fails and you must specify the type argument explicitly:
var result = Filter<int>(null, x => x > 1); // compiles, throws at runtime if source is null
Allocation and Performance Considerations
Each delegate instance allocates on the managed heap, and closures capture local variables by reference, extending their lifetime. A lambda that captures a loop variable or a method parameter prevents that variable from being collected until the delegate is released.
for (int i = 0; i < 1000; i++) { var handler = new Func<int>(() => i); // captures i // handler stays alive while used }
In hot paths, repeated delegate allocation can add measurable garbage-collection pressure. Caching a delegate instance outside the loop avoids repeated allocation when the delegate does not depend on changing state:
Func<int, int> square = x => x * x; for (int i = 0; i < 1000; i++) { var result = square(i); }
The delegate is created once and reused. The same principle applies to method group conversions, which also allocate a delegate per conversion.
Common Mistakes with Generic Delegates
One frequent error is mixing up the order of type parameters in Func. The return type is always last, so Func<int, string> means "takes an int, returns a string", not the reverse. Misreading this order produces compile errors that are easy to fix once the pattern is clear.
Another mistake is declaring a custom generic delegate when a built-in type already fits. A delegate named public delegate bool Check<T>(T item) duplicates Predicate<T>. The extra type adds no value and forces readers to learn a new name for a familiar shape.
Variance misuse also causes subtle failures. A delegate with out on a type parameter that appears in an input position will not compile, because the compiler rejects contravariant usage in a covariant position. The error message points directly at the offending parameter, so the fix is usually to remove the modifier or adjust the signature.
When a Custom Generic Delegate Is Justified
Custom generic delegates make sense when the signature is domain-specific and the name communicates intent better than Func or Action. For example, an event handler that passes a sender and a payload benefits from a named delegate:
public delegate void ItemProcessed<T>(object sender, T item);
The name tells the reader what the delegate represents, and the generic type parameter keeps it reusable across item types. This is the same reasoning that led the framework to define EventHandler<TEventArgs> rather than forcing every consumer to use Action<object, TEventArgs>.
The tradeoff is that custom delegates require their own declaration, which adds a line of code and a name to maintain. When the signature is generic enough that Func or Action reads clearly, the built-in types keep the code shorter and more familiar to other developers.