A delegate is a variable that holds a method — so "what to do" can be decided somewhere else, and passed in like any other piece of data.
You already know how to write a method: CalculateTotal(), SendEmail(), Validate(). You call it by name, and it runs. But what if you don't know, at compile time, which method should run?
Imagine you're writing a button. When the user clicks it, something should happen — but a generic Button class has no idea what. Maybe it saves a file. Maybe it opens a dialog. Maybe it closes the app. The button's author can't hardcode any of that; they need a way to say "run whatever method the caller hands me."
That's the exact problem a delegate solves. In every language you've used so far, a method has been something you call. A delegate lets a method become something you can hold in a variable, pass as an argument, and call later — a value, just like an int or a string.
This lesson is your first real exposure to treating code itself as data — the idea that unlocks lambdas, events, and eventually LINQ.
A delegate is a type that represents "a method with a particular shape" — a specific list of parameter types and a return type. A variable of that delegate type doesn't hold a number or a string; it holds a reference to a method. You can then call that method through the variable, without ever writing the method's real name at the call site.
People often describe a delegate as a "type-safe function pointer." That's a good starting mental picture: like a C function pointer, it lets you store and invoke a method indirectly — but unlike a raw pointer, the compiler checks that the method you assign actually matches the required signature.
In C#, a delegate type is declared with the delegate keyword. It describes a method signature — return type and parameters — but no body:
public delegate int MathOperation(int a, int b);
This single line defines a new type, MathOperation. Any method that takes two int parameters and returns an int is compatible with it — regardless of the method's actual name or which class it lives on. A variable of type MathOperation can be assigned any such method, and invoking the variable invokes whichever method it currently holds.
Add(2, 3) always calls Addoperation(2, 3) calls whatever method operation currently referencesSome code needs to run a piece of logic it doesn't own. Think about:
Without delegates, the only way to make code like this configurable is with interfaces (define an IComparer, implement a whole class just to wrap one method) or giant if/switch statements baked into the reusable code itself — which defeats the point of reusability.
A delegate lets the caller hand over the actual behavior as a value. The reusable code doesn't need to know what the method does — only that it matches the expected signature. This is often called a callback: "call me back with this method when you're ready."
Delegates decouple what happens from when and where it happens. The button doesn't decide what a click does. The sort routine doesn't decide how two items compare. They just agree on a signature, and let the caller supply the actual logic — as data, passed at runtime.
Report.Generate()PrintToConsole()
Report.Generate(outputMethod)outputMethod points to
public delegate int MathOperation(int a, int b);
static int Add(int a, int b) => a + b;
MathOperation operation = Add;
Add, not Add() — you're referencing the method, not calling itint result = operation(2, 3); // calls Add(2, 3) → 5
operation(2, 3) is shorthand for operation.Invoke(2, 3)using System;
// 1. Declare the delegate type
public delegate int MathOperation(int a, int b);
class Program
{
static int Add(int a, int b) => a + b;
static int Multiply(int a, int b) => a * b;
static void Main()
{
// 2. Assign a method to a delegate variable
MathOperation operation = Add;
Console.WriteLine(operation(2, 3)); // 5
// 3. Reassign — same variable, different method
operation = Multiply;
Console.WriteLine(operation(2, 3)); // 6
// 4. Pass the delegate into another method
RunOperation(operation, 4, 5);
}
static void RunOperation(MathOperation op, int x, int y)
{
// This method has no idea whether it's adding, multiplying, or anything else.
int result = op(x, y);
Console.WriteLine($"Result: {result}");
}
}
Code → Meaning → Result: operation doesn't know it's holding Add — it only knows it holds "some method that takes two ints and returns an int." Reassigning operation = Multiply instantly changes what operation(2, 3) does, with zero changes to RunOperation. That's the whole point: RunOperation is reusable for any matching behavior.
Consider an order-processing pipeline that needs to run a "validation step" before saving. Different order types need different validation, but the processing method itself shouldn't need to change every time a new rule is added.
using System;
public delegate bool OrderValidator(decimal orderTotal);
public class OrderProcessor
{
public void Process(decimal orderTotal, OrderValidator validate)
{
Console.WriteLine($"Processing order for {orderTotal:C}...");
if (!validate(orderTotal))
{
Console.WriteLine("Order rejected by validation rule.");
return;
}
Console.WriteLine("Order saved.");
}
}
class Program
{
static bool RejectIfOverCreditLimit(decimal total) => total <= 5000m;
static bool RejectIfZeroOrNegative(decimal total) => total > 0m;
static void Main()
{
var processor = new OrderProcessor();
processor.Process(1200m, RejectIfOverCreditLimit); // passes
processor.Process(9000m, RejectIfOverCreditLimit); // rejected
processor.Process(1200m, RejectIfZeroOrNegative); // passes — different rule, same Process method
}
}
OrderProcessor.Process never needs to know which validation rule is being applied. New rules can be added anywhere in the codebase without ever touching OrderProcessor — a small taste of the flexibility that makes delegates so central to extensible design.
A delegate type is like a job description: "must accept two numbers, must return a number." It doesn't say who does the job.
A delegate instance is like hiring someone for that role — you point the job description at an actual person (a method). You can fire them and hire someone else who fits the same job description (reassign the delegate), and everyone who interacts with "whoever holds this role" doesn't need to know who it currently is.
The signature is the contract; the assigned method is whoever is currently fulfilling it.
A delegate isn't a magic language trick — it's a real object.
delegate declaration compiles into a sealed class derived from System.MulticastDelegate (which itself derives from System.Delegate).MathOperation is not just a signature — it's a real reference type with Invoke, BeginInvoke/EndInvoke (legacy async pattern), and constructor members generated for you.null for a static method) and a pointer to the method to invoke.MathOperation op = Add; for a static method, the target is null and the method pointer refers to Add.obj.SomeMethod), the target is obj — so invoking the delegate later still calls the method on the correct instance, even though obj isn't mentioned at the call site.operation(2, 3) compiles to a call to the generated Invoke method.Invoke reads the stored target and method pointer, and dispatches the call — effectively a method call performed through one extra layer of indirection.A delegate type describes a signature. A delegate variable holds a reference to a method that matches that signature. The method itself still lives wherever it was defined — the delegate is just a way to refer to it indirectly.
operation = Add; assigns the method itself to the delegate. operation = Add(); would try to call Add immediately (and fails to compile here, since Add needs arguments) and assign its result — a completely different thing. Leaving off the parentheses is what tells the compiler "I mean the method, not its result."
An interface with one method (like IComparer<T>) and a delegate can often solve the same problem. The difference: an interface requires a whole class to implement it; a delegate lets you point directly at any matching method — including one that already exists — without writing a wrapper class. As you'll see in the next lessons, C# leans heavily on delegates (and lambdas) for exactly this reason.
A delegate variable that's still null throws a NullReferenceException the moment you try to invoke it — there's no method to call.
MathOperation operation = null;
operation(2, 3); // NullReferenceException
Check for null first, or use the null-conditional invocation operator: operation?.Invoke(2, 3);.
Writing public delegate int MathOperation(int a, int b);, public delegate bool Validator(decimal d);, and dozens more like it clutters your codebase with near-identical types.
The BCL already ships generic delegate types — Action and Func — that cover almost every shape you'll need. You'll meet them in the next two lessons; custom delegates are still useful, but reach for them less often than you might expect.
Trying to assign a method whose parameter or return types don't line up with the delegate's signature is a compile-time error — there's no implicit narrowing of parameter types.
Match the signature precisely (return type and parameter types, though parameter names don't need to match) — the compiler enforces this for you, which is exactly what makes delegates "type-safe."
public delegate ... by hand. You'll mostly use the built-in Action and Func delegate types (next two lessons) or write a lambda directly. Understanding what a delegate is, though, is essential — it's the concept everything else in this module builds on.
int.
delegate, assign a matching method to a variable of that type (no parentheses), and invoke it like a normal method call.MulticastDelegate) storing a target instance and a method pointer.Action/Func delegates rather than custom ones — but every one of them works exactly like the mechanism you just learned.You've seen what a delegate is and why it exists. Let's check your understanding before moving on to writing your own delegate types.
1. What does a delegate type actually describe?
Correct: B
Why B is correct: A delegate type is a contract about shape — parameter types and return type. Any method that matches, regardless of name or where it's declared, can be assigned to a variable of that delegate type.
Why A is incorrect: A delegate type doesn't fix which method will run — that's decided when you assign a variable of that type, and it can be reassigned.
Why C is incorrect: Delegates are reference types you instantiate and assign methods to; you don't inherit from them.
Why D is incorrect: Delegate variables hold method references, not numeric values.
Reinforcement: The delegate type is the "job description"; the assigned method is who currently does the job.
2. Given public delegate bool Check(int n); and static bool IsEven(int n) => n % 2 == 0;, which line correctly assigns the method to a delegate variable?
Correct: B
Why B is correct: Writing the method name without parentheses references the method itself (a "method group"), which the compiler converts into a delegate instance pointing at IsEven.
Why A is incorrect: IsEven() calls the method immediately, which requires an int argument and wouldn't even compile here — and even if it did, you'd be assigning a bool result, not a method reference.
Why C is incorrect: That's not valid delegate construction syntax for a method group assignment — you don't "new up" a delegate without pointing it at a method.
Why D is incorrect: A string is not a method reference; delegates aren't assigned by method name as text.
Reinforcement: Omit the parentheses to reference a method as a value instead of calling it.
3. Why do delegates matter for writing reusable, flexible code?
Correct: B
Why B is correct: A method that accepts a delegate parameter can run whatever logic the caller supplies, without being rewritten for every new use case — that's the core value of treating behavior as data.
Why A is incorrect: An indirect call through a delegate has a small amount of extra overhead compared to a direct call, not less.
Why C is incorrect: Delegates still take whatever parameters their signature defines; they don't eliminate parameters.
Why D is incorrect: Thread-safety is a separate concern; a delegate by itself provides no automatic synchronization.
Reinforcement: The value of delegates is flexibility and decoupling, not performance or safety guarantees.
4. A delegate variable holds a reference to an instance method, myAccount.Withdraw. What happens internally that lets invoking the delegate later still act on the correct myAccount object?
Correct: A
Why A is correct: A delegate instance stores both a target object reference and a method pointer. For an instance method, the target is the object the method should run on — so invocation later correctly dispatches to that same instance.
Why B is incorrect: Only a reference to the object is stored, not a copy — the delegate and any other code holding a reference to myAccount see the same underlying object.
Why C is incorrect: Instance methods are commonly used with delegates; this is exactly the mechanism that makes it work.
Why D is incorrect: Nothing is re-created — the delegate simply dispatches to the already-existing object it was given when assigned.
Reinforcement: "Target + method pointer" is the internal shape of every delegate instance, whether the method is static or instance-based.
You now understand what a delegate is and why C# needs a way to treat methods as values. Next, you'll write your own custom delegate types and discover multicast delegates — the mechanism events are built on.
dotnetmadeeasy.com — Learn C# and .NET, the right way.