Last Updated: May 17, 2026
A delegate is a type-safe reference to a method. Where a variable of type int holds a number and a variable of type Product holds a reference to a product, a variable of a delegate type holds a method you can call later. That single idea lets you pass behavior around the way you already pass data, and it's the foundation everything else in this section builds on (multicast, Func/Action, lambdas, and events).
A delegate is a type. Specifically, it's a type whose values are method references. You declare what shape of method the type can hold (its return type and parameter list), and then you can store any method matching that shape in a variable of that delegate type. Calling the variable runs whichever method it currently points to.
If you've worked with function pointers in C or method references in other languages, this is the same idea with a stronger type system around it. The compiler knows the exact signature the delegate expects and refuses to bind any method that doesn't match. There's no string lookup, no reflection, no runtime guesswork. The delegate type itself encodes the contract.
Three things are happening in that program. The line delegate decimal PriceAdjuster(decimal subtotal); declares a new type called PriceAdjuster. The line PriceAdjuster adjuster = ApplyTenPercentOff; creates a value of that type by pointing it at an existing method. The line adjuster(49.99m) invokes whichever method the delegate currently points to, which happens to be ApplyTenPercentOff.
The variable adjuster is not the method ApplyTenPercentOff. It's a handle that knows how to call that method. You could reassign adjuster to point at a different method later, and adjuster(49.99m) would call the new one instead. The call site doesn't change. The behavior behind it does.
A delegate carries two pieces of state: the method it should call, and (for instance methods) the object that method should run on. When the delegate is invoked, the runtime uses both pieces to dispatch the call. We'll see the instance side a few sections in. For now, every example uses static methods so there's no second piece to track.
The reason delegates exist in a language that already has methods is that methods themselves aren't first-class values in C#. You can't pass a method as an argument the way you pass an int, and you can't return a method from another method. Delegates are the wrapper that gives method references first-class treatment: assignable, passable, returnable, storable in fields.
This lesson covers the basics: declaring a delegate type, instantiating one by referencing a method, invoking it, passing it around, and handling the null case safely.
A delegate declaration looks like a method signature with the delegate keyword in front and a semicolon at the end. There's no body, because a delegate type doesn't have an implementation. It only describes what shape of method can be stored in a variable of that type.
That line declares a type named PriceAdjuster. Any method that takes a single decimal and returns a decimal is compatible with this type. Methods with different parameter types, different parameter counts, or a different return type are not.
The declaration can live wherever a class or struct can live: inside a namespace at the top level, or nested inside a class. Most teams declare delegate types at the namespace level (or just inside a class as a public nested type) so they're easy to share across the codebase.
Four delegate types, each describing a different shape of method. PriceAdjuster takes a decimal and returns one. OrderFormatter takes an int and a string and returns a string. StockCheck takes two ints and returns a bool. CartObserver takes a string and a decimal and returns void (no return value).
The compiler turns each delegate declaration into a real type behind the scenes, deriving from System.MulticastDelegate. That type has a hidden constructor, an Invoke method matching your declared signature, and a few other members. You don't write any of that yourself; the delegate keyword is the shorthand.
The base type confirms what's going on. PriceAdjuster is a full-fledged class, not just a name. You can use it anywhere you'd use a class: as a field type, as a parameter type, as a return type, as a property type.
A delegate type can also be generic, though the built-in Func<> and Action<> handle the generic cases. For now, the custom-named delegate types above are clearer for teaching, and you'll see them in real codebases for the same reason: a named type tells the reader what the delegate is for, not just what its signature is.
A delegate type is just a type, so creating a value of that type follows the usual pattern: you call a constructor, or you assign something the compiler can convert to it. C# supports two ways, and the second one is much more common.
The verbose, explicit form uses new DelegateType(method):
That syntax mirrors how you'd construct any other object. It's useful when you want to be explicit about which delegate type you're creating, especially when the same method could be assigned to several different delegate types.
The shorter, idiomatic form drops the constructor and just assigns the method name to the delegate variable. The compiler converts the method reference (called a method group) into a delegate of the target type automatically:
This is called a method group conversion. The expression ApplyTenPercentOff on its own refers to a method group, which is the set of all overloads with that name. When the compiler sees it being assigned to a variable of delegate type, it picks the overload whose signature matches the delegate and constructs the delegate for you. It's identical in effect to the explicit new PriceAdjuster(...) form; just shorter.
Both forms work the same way. The implicit conversion is what you'll write day to day; the explicit constructor is what you'll see in older code or in places where the compiler can't infer the target type (most commonly, when you're passing a method group as an object).
Instance methods work the same way, with one detail: the delegate captures the target object along with the method.
Both delegates point at the same method, DiscountPolicy.Apply, but each one carries a different target object. lightDiscount calls Apply on the tenOff instance; heavyDiscount calls it on twentyOff. The delegate remembers both pieces, so the call site lightDiscount(50m) doesn't need to mention the object at all.
That's the part that makes delegates more powerful than raw function pointers. A delegate built from an instance method has the object baked in. You can hand the delegate to code that knows nothing about DiscountPolicy, and that code can still invoke the right behavior for the right instance.
Cost: Constructing a delegate allocates a small heap object that holds the method pointer and the target reference. It's a single allocation per delegate value, not per invocation, but it's still an allocation. In hot paths, cache the delegate in a field instead of building a new one on every call.
A method group conversion is a one-time event at the point of assignment. After the conversion, you have a real delegate object, and invoking it doesn't construct anything else. The cost is on the way in, not on the way through.
Once a delegate has a value, calling it looks exactly like calling a regular method. You write the variable name, an argument list in parentheses, and the runtime dispatches to the captured method on the captured target.
The call adjuster(50m) looks like any other method call. Behind the scenes, the compiler emits a call to the delegate's Invoke method, passing the arguments through. The runtime then dispatches to the method the delegate is pointing at.
You can write that explicit form yourself, and it produces identical code:
The two forms (adjuster(50m) and adjuster.Invoke(50m)) are completely interchangeable. The shorter form is what most code uses, because there's no point in spelling out .Invoke when the parentheses already mean "call this." The .Invoke form shows up in a few places: documentation, generated code, and the null-conditional pattern we'll see in a moment.
The return value of the call is the return value of the underlying method. If the delegate's signature returns void, the call doesn't produce a value:
The delegate signature returns void, and the call is treated as a statement, not an expression. You can't write var x = observer(...) because there's no value to assign.
The dispatch itself is fast. Invoking a delegate is one indirect call (read the method pointer out of the delegate object, jump to it). It's slower than a direct method call by a few nanoseconds on modern hardware, but the difference rarely matters outside very tight inner loops. The cost worth watching is the allocation when the delegate is created, not the cost of calling it once it exists.
Output (example):
Ten million invocations finish in tens of milliseconds. The exact number depends on the machine, but the point is the same: a delegate call isn't free, but it's not slow enough to design around in normal code. Save the worry for hot inner loops in numerical or graphics code, and even there, measure before changing.
Once you have a delegate type, you can use it anywhere a type fits. The most common use is as a method parameter: a method takes a delegate, and the caller passes in whichever behavior they want the method to use.
CalculateCartTotal has no idea which discount is being applied. It sums the prices, then defers the final step to whichever PriceAdjuster the caller hands in. Three different call sites pass three different methods, and the same CalculateCartTotal produces three different totals.
This is the part that makes delegates useful even before you reach lambdas. You can swap behavior at the call site without changing the method that uses it, and you can plug different strategies into the same algorithm without touching the algorithm itself.
The method group conversion happens at every call site. Writing CalculateCartTotal(cart, TenPercentOff) is shorthand for CalculateCartTotal(cart, new PriceAdjuster(TenPercentOff)). The compiler picks the matching overload of TenPercentOff and builds the delegate.
Delegates can be stored in fields and properties too. A class can hold a delegate so its behavior can be swapped at runtime:
CheckoutCalculator doesn't know or care which discount it applies. It holds a PriceAdjuster and runs it when asked. The caller can change the strategy by assigning a different delegate to the property.
A delegate can also be a method's return value. A factory method picks the right behavior based on some condition and hands the delegate back:
GetAdjuster is a method that returns a method. The caller asks for the right discount strategy and gets back a delegate they can call. The selection logic lives in one place; the call site stays simple.
A diagram makes the relationship between a delegate variable and its target concrete:
The variable adjuster is a reference to a delegate object on the heap. The delegate object holds the pointer to the method (TenPercentOff in this case) and a slot for the target instance, which is null for a static method and a reference to the owning object for an instance method. Calling adjuster(50m) follows the chain: from the variable to the delegate object, from the delegate object to the method, and then the method runs with the arguments you passed.
For an instance method, the picture is the same with one extra arrow:
The delegate now points at both the method and the object the method should run on. When lightDiscount(50m) fires, the runtime knows to call DiscountPolicy.Apply with tenOff as the receiver, plus 50m as the argument.
A delegate is a reference type. Like any reference type, the variable can hold null, which means "no method is attached." Invoking a null delegate throws NullReferenceException at runtime, the same way calling a method on any null reference does.
That's a bug you'll hit any time a delegate field is declared but never assigned, or a delegate parameter is left null by a careless caller. The fix is to check before you call.
The naive check is an explicit if:
That works, and you'll see code like it. The downside is that in multi-threaded code, the delegate could become null between the check and the call. Thread A reads adjuster, sees it's non-null, and prepares to invoke. Thread B clears the field. Thread A calls the now-null delegate and crashes. This pattern is rare in straight-line code but common with events, covered later in this section.
The modern idiom uses the null-conditional operator ?. together with the explicit Invoke call:
The expression OnItemAdded?.Invoke(name, price) reads the field once into a temporary, checks the temporary for null, and either invokes through the temporary or short-circuits and returns. The thread-safety hole closes because there's only one read of the field, and the temporary cannot become null after the check.
You have to write ?.Invoke(...) rather than ?(...) because the language doesn't define a null-conditional call syntax on a value directly. The explicit Invoke is what gives the operator something to attach to.
Cost: ?.Invoke(...) compiles to almost the same code as the manual if (d != null) d(...) check, with the added benefit of reading the field exactly once. There's no measurable performance difference, so use the ?. form by default.
For a method that takes a delegate as a parameter, the convention is usually the opposite: require the delegate to be non-null and throw early if it isn't. Optional behavior is what events and field-based delegates use the ?.Invoke pattern for. Required behavior is what a parameter is, and a null parameter is a caller bug worth reporting.
ArgumentNullException.ThrowIfNull was added in .NET 6 and is the cleanest way to validate parameters. It also captures the parameter name automatically using CallerArgumentExpression, so the error message mentions adjuster without you having to spell it out.
Everything in this lesson works with a single method attached to a single delegate. That's the smallest useful piece, and almost every C# program uses delegates this way: a callback, a strategy, a filter, a comparer, a sort key, an event handler. Once you understand "a variable that holds a method reference," the rest builds on that idea.
A delegate can hold more than one method, with += adding to its invocation list and -= removing methods, so a single call fans out to every subscribed method in order. That's the mechanism events use under the hood.
Declaring a new delegate type for every signature gets tedious quickly. The built-in generic delegate types Func<> and Action<> cover almost every case, so you don't need to write delegate decimal PriceAdjuster(decimal subtotal); for every shape of method you want to pass around.
Writing a named method just to assign it to a delegate is also more ceremony than the work usually deserves. Lambda expressions let you write the method body directly in the place where the delegate is being created: PriceAdjuster adjuster = subtotal => subtotal * 0.9m;. That's the form you'll see in most modern code, including LINQ, async, and ASP.NET.
Events wrap a delegate in a controlled access pattern: outside code can subscribe with += and unsubscribe with -=, but it can't read the invocation list or invoke it directly. Only the class declaring the event can raise it. That's the right shape for a notification mechanism like "the cart contents changed," where you want to announce things without exposing the subscriber list.
For now, the takeaway is the model: a delegate type names a method shape, a delegate value is a method reference of that shape, and you can pass it, store it, return it, and invoke it just like any other value. The rest is decoration.
public delegate decimal PriceAdjuster(decimal subtotal); creates a new type derived from System.MulticastDelegate. The compiler generates the constructor and Invoke method behind the keyword.PriceAdjuster a = MyMethod;), which is equivalent to the explicit new PriceAdjuster(MyMethod) constructor call.a(args)), which compiles to a call to the delegate's Invoke method (a.Invoke(args)). Both forms are interchangeable; the parentheses form is what most code uses.NullReferenceException. Use myDelegate?.Invoke(args) for optional callbacks and ArgumentNullException.ThrowIfNull(myDelegate) for required parameters.Func<> and Action<> cover most signatures.The _Multicast Delegates_ lesson shows how a single delegate variable can hold a list of methods, how += and -= build and unwind that list, and how invocation order, return values, and exceptions behave when more than one method is attached.