Skip to main content
๐Ÿ“œ WAYPOINT LESSON

The delegate machine

โญโญโญ advancedโณ 14 min read๐Ÿ“ Lesson 171 of 265

Delegate objects, method groups, invocation lists, and what each lambda compiles into.

The delegate machine

A delegate is an object: it holds a method (or, for virtual/interface calls, a function pointer plus the receiver object) and an optional invocation list. Foo as an expression ("method group") converts to a delegate instance; a lambda compiles to a compiler-generated method wrapped in a delegate:

Func<int, int> f = x => x + 1;   // compiler generates: static int <>M0(int x)
Func<int, int> g = doubleIt;     // method group โ†’ existing method, no new code
Func<int, int> h = f + g;        // MulticastDelegate: invocation list [f, g]

Allocation profile you can predict:

| Construct | Compiles to | Allocates | |---|---|---| | static lambda, no capture | static method + delegate | 1 delegate | | lambda capturing a local | display class + method + delegate | 1 class + 1 delegate | | method group (instance) | delegate to that instance's method | 1 delegate | | static lambda modifier | guarantees no capture | 1 delegate |

The static lambda modifier is a contract: static x => x + 1 refuses to compile if it captures anything โ€” a cheap way to make "no hidden closure" an enforced property, not a code-review promise.

Invocation order in a multicast delegate is defined (first added first invoked); the return value you keep is the LAST handler's. Caching delegate instances (static readonly Func<...>) avoids re-allocating per call โ€” this is the single biggest avoidable allocation in lambda-heavy code.