Core Definition
In C#, a delegate serves as a type-safe reference container for executable routines. Unlike conventional objects that primarily hold state, delegates encapsulate method addresses and enable invocation behavior. When triggered, a delegate sequentially executes every routine bound to it, assuming all targets adhere to the delegate's declared contract.
Fundamental Characteristics
A delegate can be visualized as a callable object managing an ordered execution queue. Key operational rules include:
- The assembled routines form a continuous invocation chain.
- Bound routines may originate from any class, struct, or static scope, provided they strictly align with the delegate's signature.
- Alignment requires identical return types and matching paramter lists, including modifiers like
reforout. - Chains can freely mix instance-based and static methods.
- Triggering the delegate forces execution of every registered routine in registration order.
Delegates vs. Traditional Classes
| Operation Phase | Standard Classes | Delegate Types |
|---|---|---|
| Type Declaration | Uses the class keyword with properties and methods |
Uses the delegate keyword defining only a signature |
| Variable Initialization | Requires constructor invocation via new |
Binds a method reference to an established delegate signature |
| Runtime Allocation | Creates an object instance on the managed heap | Instantiates a System.Delegate derived wrapper pointing to target methods |
| Execution Model | Access fields/members through dot notation | Execute directly using parentheses or the built-in Invoke() method |
Declaration Syntax
Delegates operate as custom reference types. The declaration establishes the behavioral contract that all bound methods must satisfy.
delegate ReturnType DelegateLabel(ParameterType arg);
// Example constraint
delegate void ComputeCallback(double input);
Distinctions from standard method definitions:
- Must begin with the
delegatekeyword. - Contains zero implementation blocks or braces.
- Can be declared at the namespace boundary, entirely independent of surrounding classes.
Instantiation Strategies
Binding a method to a delegate supports two explicit pathways:
Constructor-Based Binding:
// Attaching an instance method
ActionPipeline handlerOne = new ActionPipeline(obj.ExecuteStep);
// Attaching a static routine
ActionPipeline handlerTwo = new ActionPipeline(BackgroundRunner.RunSilent);
Implicit Assignment:
// Compiler automatically infers the delegate wrapper type
ActionPipeline implicitOne = obj.ExecuteStep;
ActionPipeline implicitTwo = BackgroundRunner.RunSilent;
Both techniques allocate a delegate object and seed its internal list with the designated entry point.
Assignment & Garbage Collection
As reference types, delegates follow standard allocation rules. Reassigning a delegate variable discards the previous reference, making it eligible for garbage collection.
TaskExecutor executor = manager.BeginWork;
executor = worker.ResumeTask; // Previous 'executor' reference orphaned and collected
Combination & Immutability
Delegates natively support addition operators to merge multiple targets into a single invocation entity.
TaskExecutor first = manager.BeginWork;
TaskExecutor second = worker.PauseTask;
TaskExecutor merged = first + second;
A critical architectural rule: delegate instance are immutable. Operators like + never mutate existing objects. Instead, they construct a fresh delegate containing the combined chain and return the new instance for assignment.
Dynamic Registration & Deregistration
Appending Routines: The += operator extends the invocation chain. Each operation allocates a new delegate instance internally.
TaskExecutor chain = initialAction;
chain += midPhase;
chain += finalPhase;
Removing Routines: The -= operator detaches routines. The runtime scans backward from the tail of the chain and removes the first matching occurrence. Deleting a non-existent entry is harmless. Invoking removal on a null delegate triggers a NullReferenceException, necessitating defensive validation.
Invocasion Mechanics
Calling a delegate mirrors direct method execution. Arguments pass identically to each bound routine.
double triggerValue = 3.14;
ComputeCallback callback = mathEngine.Scale;
callback(triggerValue); // Syntactic shorthand
callback.Invoke(triggerValue); // Explicit method call
If a routine repeats across the chain, it fires multiple times upon encountering its positions. Null delegates throw exceptions during execution; safe invocation patterns prevent crashes:
// Modern safe invocation
callback?.Invoke(triggerValue);
// Legacy verification
if (callback != null) { callback(triggerValue); }
Handling Return Values
When a delegate encapsulates multiple routines sharing a non-void return type, execution semantics shift. Only the result produced by the last executed routine propagates outward. All intermediate return values are discarded.
delegate int YieldResult();
Calculator calc = new Calculator();
YieldResult pipeline = calc.BaseValue;
pipeline += calc.ApplyMultiplier;
pipeline += calc.ApplyOffset;
Console.WriteLine(pipeline?.Invoke()); // Outputs offset calculation only
Reference Parameter Propagation
Delegates fully respect ref and out modifiers. State mutations persist across the entire chain, meaning each subsequent routine receives the updated argument state rather than the original value.
delegate void ModifyState(ref double data);
Transformer engine = new Transformer();
ModifyState processor = engine.Negate;
processor += engine.Absolute;
double baseline = -15.5;
processor?.Invoke(ref baseline);
Console.WriteLine(baseline); // Final mutated value displayed