Back to Blog
C#

C# Delegate Declaration Explained

c# delegate declaration: Learn how to declare delegates in C#, use named and anonymous methods, and apply Action and Func for cleaner code.

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 c# delegate declaration 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 an identical signature, meaning the same parameter types (and ref/out modifiers if present) and the same return type. The method name does not matter.

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

If the method has a different signature, 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 at namespace level is accessible throughout that namespace (subject to access modifiers). 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.

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; would fail because there is no delegate type to infer from. Always provide the explicit delegate type or a parameter type in the lambda.

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 notify = () => Console.WriteLine("First"); notify += () => Console.WriteLine("Second"); notify(); // Prints First then Second notify -= firstMethod; // Removes the first

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 Func and Action delegates, the return value of a multicast delegate is the return value of 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.

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(new Comparison<T>(comparer)); } 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 Methods Groups

Method groups—when you reference a method by name—can be implicitly converted to a delegate type if the signature matches. Lambda expressions also convert implicitly. However, variables declared with var cannot be used for lambda expressions without an explicit type. Keep this in mind when you write code that assigns a lambda to a var.

// 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 mismatch of return type. 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. Also, when using multicast delegates, remember that the invocation order is not guaranteed across different delegate instances—but the order for a given list is stable. Finally, a delegate instance that is invoked while another method is modifying its invocation list can produce unpredictable behavior; in a multithreaded context, consider taking a snapshot of the delegate before invocation.

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.

A delegate that is not invoked in a thread-safe manner can cause race conditions if one thread modifies the invocation list while another invokes it. The common pattern is to copy the delegate to a local variable before checking for null and invoking.

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