Back to Blog
C#

C# Delegate Declaration Explained

Learn how to declare and use C# delegates: signature matching, named methods, lambdas, multicast behavior, built-in Action and Func types, and events.

delegatesC#lambda expressionsmethod referencesevent handling
Illustration of a C# delegate declaration as a method signature, with a box representing a method reference and an arrow connecting to a target method.

When you write a delegate declaration in C#, you are defining a type that holds a reference to a method with a specific signature. That signature determines which methods can be assigned to the delegate, and it allows you to pass methods as parameters, store them in fields, and invoke them later. The declaration looks like a method signature but uses the delegate keyword.

public delegate int Processor(int input);

This declares a delegate type named Processor that can reference any method taking a single int parameter and returning an int. Once declared, you can create an instance of this delegate and assign a method to it.

static int Double(int x) => x * 2; Processor process = Double; int result = process(5); // 10

The delegate instance process holds a reference to the Double method. You invoke it like a regular method call. This is the foundation of many callback patterns in C#.

Delegate Types and Method Signature Matching

A delegate type is defined by its parameter list and return type. Methods assigned to a delegate must have a compatible signature. In the common case, the method has the same parameter types and the same return type. C# also allows variance for method-group conversions: a method can use a parameter type that is less derived than the delegate's parameter type, and it can return a type that is more derived than the delegate's return type. ref and out modifiers must match exactly; they do not participate in variance.

public delegate bool Filter(string value); static bool IsLong(string s) => s.Length > 10; static bool StartsWithA(string s) => s.StartsWith("A"); Filter f = IsLong; f = StartsWithA; // Valid: same signature

For example:

delegate void StringAction(string s); static void HandleObject(object o) { } StringAction action = HandleObject; // Valid: parameter is less derived delegate object GetValue(); static string GetName() => "name"; GetValue getValue = GetName; // Valid: return type is more derived

If the method's signature is not compatible, the compiler rejects the assignment. For example, a method that accepts two parameters cannot be assigned to a delegate that takes one.

Declaring a Delegate Inside a Class or at Namespace Level

Delegate declarations can appear inside a class or directly in a namespace. The scope affects visibility. A delegate declared inside a class is nested within that type. One declared directly in a namespace is a namespace member whose accessibility is controlled by its access modifier. For utility delegates used across an application, declaring them at namespace level is common. For delegates only used within a single class, keep them nested to avoid polluting the namespace.

namespace MyApp { public delegate void Logger(string message); public class Service { // Uses Logger from namespace level public void Run(Logger log) { log("Starting"); } } }

Using Delegates with Named Methods

Named methods are straightforward: you assign a method name to a delegate instance. This works for static methods, instance methods, and even private methods if you are within the same class.

public delegate int BinaryOperation(int a, int b); class Calculator { public int Add(int a, int b) => a + b; public static int Subtract(int a, int b) => a - b; } // Instance method Calculator calc = new Calculator(); BinaryOperation op = calc.Add; // Static method BinaryOperation op2 = Calculator.Subtract;

Named methods are clear and easy to read, especially when the method has a meaningful name and is reused in multiple places.

Anonymous Methods and Lambda Expressions

Instead of naming a separate method, you can provide an inline anonymous method or a lambda expression. The delegate keyword allows an anonymous method block.

Processor p = delegate (int x) { return x * x; };

Lambda expressions are more concise and are the preferred syntax in modern C#.

Processor p = x => x * x;

When you assign a lambda to a delegate type, the compiler infers parameter types from the delegate signature. That inference only works if the target type is known, so var p = x => x * x; fails because there is no delegate type to infer from. Declare the delegate type explicitly. In C# 10 and later, a lambda with explicit parameter types and an inferable return type has a natural type, so var p = (int x) => x * x; is valid when you need to use var.

Using Built-in Delegate Types: Action and Func

Most of the time you don't need to define your own delegate types. The .NET framework provides generic delegates that cover virtually every method signature.

  • Action<T1, T2, ...> for methods that return void (up to 16 parameters).
  • Func<T1, T2, ..., TResult> for methods that return a value (up to 16 input parameters plus a return type).
Func<int, int> square = x => x * x; Action<string> print = s => Console.WriteLine(s);

Using Func and Action reduces the number of custom delegate declarations in your codebase and makes the signatures immediately obvious. However, when a delegate will be reused frequently or has a meaningful domain-specific name, a custom delegate can improve readability.

Multicast Delegates

Delegates in C# are multicast, meaning a single delegate instance can hold references to multiple methods. The + operator combines delegates, and - removes one.

Action first = () => Console.WriteLine("First"); Action second = () => Console.WriteLine("Second"); Action notify = first; notify += second; notify(); // Prints First then Second notify -= first; notify(); // Prints Second

Invocation order is the order in which methods were added. If a method throws an exception during invocation, the remainder of the invocation list is not called. That behavior often matters in event handling scenarios.

For a multicast delegate with a return type, the returned value is the value returned by the last method in the invocation list, which can be surprising if you combine methods that return values. In such cases, consider whether multicast behavior is actually intended.

If you need to remove a handler later, store the delegate in a variable. Equal-looking lambda literals are not the same delegate instance, so removing with a newly written lambda literal does not necessarily remove the original handler.

Delegate Declaration and Events

Events are based on delegates but add restrictions. An event is a member that can only be invoked from within the declaring class, and external code can only add or remove handlers using += or -=. This encapsulation prevents external code from clearing the invocation list or invoking the event directly.

class Button { public event Action Clicked; public void SimulateClick() { Clicked?.Invoke(); } }

Even though events use delegate types under the hood, the delegate declaration itself doesn't enforce event semantics. The event keyword does.

Passing Delegates as Parameters

One of the most common uses of delegates is passing behavior into a method. This enables callbacks and strategy patterns. For example, a sorting method could take a delegate that determines ordering.

void Sort<T>(List<T> list, Func<T, T, int> comparer) { list.Sort((x, y) => comparer(x, y)); } Sort(numbers, (a, b) => a.CompareTo(b));

Here the delegate is a parameter that lets the caller control the sorting logic. The Sort method only needs to know the callback signature, not the concrete method.

Delegate Compatibility with Methods, Lambdas, and Method Groups

Method groups—when you reference a method by name—can be implicitly converted to a delegate type if the signature is compatible. Lambda expressions also convert implicitly when the target type is known. A lambda with no explicit parameter types has no natural type, so var f = x => x; is invalid. Declare the delegate type explicitly, or use explicit parameter types in a C# 10+ lambda that has a natural type.

// Invalid: var cannot be used because the lambda has no target type // var f = x => x; // Valid Func<int, int> f = x => x;

Performance and Allocation Implications

Allocating delegate instances has a small runtime cost. When a lambda captures variables from its enclosing scope, it may allocate a closure object on the heap. That allocation happens once per invocation of the enclosing method, not per delegate invocation. In tight loops, capturing a variable can lead to repeated allocations. Reusing a static method or a cached delegate avoids that.

// Captures local variable 'factor' Func<int, int> multiplier = x => x * factor; // Better for repeated use if factor is constant static int MultiplyByTwo(int x) => x * 2;

If you are adding many event handlers or doing heavy performance-sensitive work, consider whether the delegate allocation is acceptable. For most application code, the overhead is negligible.

Common Mistakes in Delegate Declarations

A frequent error is a return type mismatch. A delegate with a void return type cannot be assigned a method that returns a value, even if the return value is ignored. Another mistake is using var with a lambda that has no target type and no explicit parameter types. Also, when using multicast delegates, remember that += and -= create new delegate instances; the original delegate instance is immutable. If you need to remove a handler, keep the delegate value you added in a variable. In multithreaded code, copy the delegate to a local variable before checking for null and invoking, so you use one consistent snapshot.

Choosing Between Custom Delegate Types and Action/Func

Always prefer Func and Action for simple callbacks. Define a custom delegate when the signature is complex or when you want to give it a domain-specific name that clarifies intent. For example, a delegate called SensorUpdateHandler is more descriptive than Action<double, long>. The tradeoff is that custom delegate types add boilerplate. The decision depends on how often the delegate is used and how much the name aids maintenance.

Runtime Behavior of Delegate Invocation

When you invoke a delegate, the runtime calls each method in its invocation list synchronously. Delegate invocation is not asynchronous by itself; if you want asynchronous operations, you use async methods or Task-based patterns. Also, the Invoke method and the () syntax are equivalent. The null-conditional operator ?.Invoke is safe in C# 6 and later. This is standard practice for events.

Delegate instances are immutable. A thread that has already retrieved a delegate variable continues to use the old delegate instance even if another thread assigns a new one. The common pattern is to copy the delegate to a local variable before checking for null and invoking, which gives one consistent snapshot and avoids the race between a null check and invocation.

Maintaining Delegate Declaration in a Codebase

To keep delegate declarations maintainable, centralize them when they are used across many classes. Avoid creating excessively long parameter lists—consider using a custom struct or class instead. Document the contract the delegate enforces: what the method should do, what the parameters mean, and what the return value indicates. This helps prevent misuse.

When using Action and Func, be careful with parameter counts. A Func with five or more parameters becomes harder to read. In such cases, a custom delegate with clearly named parameters is better. Also, avoid mixing ref and out parameters in delegates unless necessary; they add complexity and cannot be used with Func or Action directly.

C# Delegate Declaration: Practical Usage and Code Examples | RYUSLOG DEV