A delegate is an object that holds a method to call later. A lambda is a short way to write one. Most bugs come from what a lambda quietly captures.
Delegates power LINQ, events, callbacks, and async continuations. This page covers how they work and where they cost memory. It also covers the lambda features added from C# 9 to C# 14. Each feature has a tested sample, traps, and interview questions.
A delegate type describes a method signature. delegate int Op(int a, int b); declares one. The BCL ships generic ones, so you rarely need your own:
Func<T1, …, TResult>: takes up to 16 inputs and returns a value.Action<T1, …>: takes inputs and returns void.Predicate<T>: an older name for Func<T, bool>, used by List.FindAll.Func and Action for most callbacks and strategy arguments.ref, out, in, or params. Those cannot be expressed with Func.RetryPolicy./// <summary>A custom delegate: needed because Func cannot express an out parameter.</summary>
public delegate bool TryParser<T>(string text, out T value);
/// <summary>A named delegate type for readability.</summary>
public delegate int BinaryOp(int a, int b);
public static class Delegates
{
/// <summary>Applies an operation to fold a list from left to right.</summary>
/// <param name="items">Values to fold. Must not be empty.</param>
/// <param name="op">How to combine two values.</param>
/// <returns>The folded result.</returns>
/// <example>Delegates.Fold([1, 2, 3], (a, b) => a * b) returns 6.</example>
public static int Fold(int[] items, BinaryOp op)
{
// [0] seeds the fold because there is no neutral value for an unknown op.
int acc = items[0];
// i is the next item to fold in. Start at 1 since item 0 is the seed.
for (int i = 1; i < items.Length; i++) acc = op(acc, items[i]);
return acc;
}
/// <summary>Parses every string with the given parser and keeps the good ones.</summary>
/// <param name="texts">Inputs.</param>
/// <param name="parse">A TryParse-shaped method, such as int.TryParse.</param>
/// <returns>Only the values that parsed.</returns>
/// <example>Delegates.ParseAll(["1", "x", "3"], int.TryParse) returns [1, 3].</example>
public static List<T> ParseAll<T>(string[] texts, TryParser<T> parse)
{
var result = new List<T>();
foreach (var t in texts)
{
if (parse(t, out T v)) result.Add(v);
}
return result;
}
/// <summary>Shows that Predicate and Func with the same shape do not convert.</summary>
/// <returns>Matches found using a Func wrapped as a Predicate.</returns>
/// <example>Delegates.PredicateVsFunc() returns [2, 4].</example>
public static List<int> PredicateVsFunc()
{
Func<int, bool> isEven = x => x % 2 == 0; // % 2 == 0 means divisible by 2
var list = new List<int> { 1, 2, 3, 4 };
// list.FindAll(isEven) does not compile. Wrap with new Predicate or a lambda.
return list.FindAll(new Predicate<int>(isEven));
}
}
Func<int, bool> and Predicate<int> have the same shape but do not convert. Wrap one in the other.NullReferenceException. Use callback?.Invoke(x).Func? A: When a parameter is ref, out, in, or params, or when a domain name makes the API clearer.Every delegate can hold a list of methods. a + b or a += b makes a new delegate that calls both, in order. -= removes the last matching entry. Delegates are immutable, so each += builds a new object.
GetInvocationList() to call each entry yourself. That lets you collect every result or keep going after an exception.public static class Multicast
{
/// <summary>Calls a multicast Func. Only the last result comes back.</summary>
/// <returns>The value returned by invoking the combined delegate.</returns>
/// <example>Multicast.LastWins() returns 3.</example>
public static int LastWins()
{
// 1, 2, 3 are labels so you can see which handler's result survives.
Func<int> all = () => 1;
all += () => 2;
all += () => 3;
return all(); // runs all three, keeps only the last result
}
/// <summary>Collects every handler's result through the invocation list.</summary>
/// <returns>All results in call order.</returns>
/// <example>Multicast.AllResults() returns [1, 2, 3].</example>
public static List<int> AllResults()
{
Func<int> all = () => 1;
all += () => 2;
all += () => 3;
return all.GetInvocationList().Cast<Func<int>>().Select(f => f()).ToList();
}
/// <summary>Shows that one throwing handler stops the rest.</summary>
/// <returns>The handlers that ran before the exception.</returns>
/// <example>Multicast.ExceptionStopsChain() returns ["a"].</example>
public static List<string> ExceptionStopsChain()
{
var ran = new List<string>();
Action chain = () => ran.Add("a");
chain += () => throw new InvalidOperationException("boom");
chain += () => ran.Add("c"); // never runs
try { chain(); } catch (InvalidOperationException) { }
return ran;
}
/// <summary>Removing a lambda with -= fails because each lambda is a new object.</summary>
/// <returns>How many handlers remain after the failed and the real removal.</returns>
/// <example>Multicast.RemoveDemo() returns "2 1".</example>
public static string RemoveDemo()
{
var log = new List<int>();
Action<int> keep = x => log.Add(x);
Action<int>? all = keep;
all += x => log.Add(-x); // -x: a second, different handler
all -= x => log.Add(-x); // a new lambda object, so nothing matches
int afterLambda = all!.GetInvocationList().Length;
all -= keep; // the same object, so this one is removed
return $"{afterLambda} {all!.GetInvocationList().Length}";
}
}
-= with a fresh lambda does nothing. Keep the delegate in a variable to remove it later.null, not empty.Func<int> return? A: The return value of the last method in the list. Use GetInvocationList to get them all.A lambda that uses a variable from an outer scope captures the variable, not its value. The compiler moves that variable into a hidden heap object, often called the display class. The method and every lambda then share that one object. Later changes are visible to all of them.
foreach bug. Before C# 5, a foreach variable was declared once for the whole loop. Every lambda saw the last item. C# 5 made it a fresh variable per pass.for bug still exists. A for loop variable is declared once, outside the body. Every lambda captures the same i, so they all see its final value.Reading the figure. Blue boxes are the delegates. Red is a single shared closure object. The loop kept bumping its i until the exit test failed at 3. Green closures are one per pass, each holding its own x. Notice the fix for for: copy i into a local inside the body. That local is fresh per pass, just like the foreach variable.
public static class Closures
{
/// <summary>The for-loop capture bug, still present in C# 14.</summary>
/// <returns>What each lambda returns when called after the loop.</returns>
/// <example>Closures.ForLoopBug() returns [3, 3, 3].</example>
public static List<int> ForLoopBug()
{
var fs = new List<Func<int>>();
// 3 lambdas. i is declared once for the whole loop and ends at 3.
for (int i = 0; i < 3; i++) fs.Add(() => i);
return fs.Select(f => f()).ToList();
}
/// <summary>The fix: copy the loop variable into a fresh local each pass.</summary>
/// <returns>Each lambda's own value.</returns>
/// <example>Closures.ForLoopFixed() returns [0, 1, 2].</example>
public static List<int> ForLoopFixed()
{
var fs = new List<Func<int>>();
// 3 passes again. copy is a new variable each pass, so each lambda gets its own.
for (int i = 0; i < 3; i++)
{
int copy = i;
fs.Add(() => copy);
}
return fs.Select(f => f()).ToList();
}
/// <summary>foreach makes a fresh variable per pass (since C# 5).</summary>
/// <returns>Each lambda's own value.</returns>
/// <example>Closures.ForeachIsFine() returns [0, 1, 2].</example>
public static List<int> ForeachIsFine()
{
var fs = new List<Func<int>>();
foreach (int x in new[] { 0, 1, 2 }) fs.Add(() => x); // 0, 1, 2 are sample items
return fs.Select(f => f()).ToList();
}
/// <summary>Two lambdas share one captured variable.</summary>
/// <returns>The value read after two increments.</returns>
/// <example>Closures.SharedCounter() returns 2.</example>
public static int SharedCounter()
{
int count = 0; // 0: nothing counted yet
Action bump = () => count++;
Func<int> read = () => count;
bump();
bump();
return read(); // both lambdas see the same count variable
}
/// <summary>Makes a counter. The local outlives the method because it is captured.</summary>
/// <returns>A function that returns 1, 2, 3, ... on each call.</returns>
/// <example>var next = Closures.MakeCounter(); next(); next() returns 2.</example>
public static Func<int> MakeCounter()
{
int n = 0; // 0 so the first call returns 1
return () => ++n;
}
}
for capture bug is common with Task.Run(() => Work(i)) in a loop. Copy i to a local first.this. Using any field captures the whole object. A long-lived delegate then keeps the object alive.ref, in, or out parameters, or ref locals.for (int i = 0; i < 3; i++) list.Add(() => i); print when you call them all? A: 3, 3, 3. There is one i, and it is 3 when the loop ends. foreach would give 0, 1, 2.Put static before a lambda, as in static x => x * 2. The compiler then forbids capturing locals, parameters, or this. Any accidental capture becomes error CS8820.
state argument on APIs that offer one, such as string.Create or ConcurrentDictionary.GetOrAdd.using System.Collections.Concurrent;
public static class StaticLambdas
{
/// <summary>Squares every number with a static lambda. No closure is possible.</summary>
/// <param name="items">Input numbers.</param>
/// <returns>The squares.</returns>
/// <example>StaticLambdas.Squares([1, 2, 3]) returns [1, 4, 9].</example>
public static List<int> Squares(int[] items) => items.Select(static x => x * x).ToList();
/// <summary>Passes outside data as state instead of capturing it.</summary>
/// <param name="cache">The cache.</param>
/// <param name="key">Key to look up.</param>
/// <param name="suffix">Extra data the factory needs.</param>
/// <returns>The cached or new value.</returns>
/// <example>StaticLambdas.GetOrAdd(cache, "a", "!") returns "a!".</example>
public static string GetOrAdd(ConcurrentDictionary<string, string> cache, string key,
string suffix) =>
// suffix travels as the state argument, so the lambda captures nothing.
cache.GetOrAdd(key, static (k, s) => k + s, suffix);
/// <summary>string.Create fills a new string in place, with state passed in.</summary>
/// <param name="c">The character to repeat.</param>
/// <param name="n">How many times.</param>
/// <returns>A string of n copies of c.</returns>
/// <example>StaticLambdas.Repeat('z', 3) returns "zzz".</example>
public static string Repeat(char c, int n) =>
string.Create(n, c, static (span, ch) => span.Fill(ch));
}
static does not ban constants or static fields. It only bans captured instance state, locals, and parameters.static just makes sure it stays non-capturing.A local function is a named method declared inside another method. It can capture outer variables like a lambda. But it is not a delegate unless you convert it to one.
ref, out, and params.static local functions (C# 8) cannot capture at all.yield.public static class LocalFunctions
{
/// <summary>Counts paths in a grid with a memoised recursive local function.</summary>
/// <param name="rows">Grid rows, at least 1.</param>
/// <param name="cols">Grid columns, at least 1.</param>
/// <returns>Paths from top-left to bottom-right moving only right or down.</returns>
/// <example>LocalFunctions.GridPaths(3, 3) returns 6.</example>
public static long GridPaths(int rows, int cols)
{
var memo = new Dictionary<(int, int), long>();
return Paths(rows - 1, cols - 1); // - 1: index of the last row and column
// A local function can call itself. A lambda would need a pre-declared variable.
long Paths(int r, int c)
{
// 0 in either index means a single straight line remains: exactly 1 path.
if (r == 0 || c == 0) return 1;
if (memo.TryGetValue((r, c), out long hit)) return hit;
// - 1 steps back one row (came from above) or one column (came from the left).
long total = Paths(r - 1, c) + Paths(r, c - 1);
memo[(r, c)] = total;
return total;
}
}
/// <summary>An iterator that validates eagerly, thanks to a local function.</summary>
/// <param name="start">First value.</param>
/// <param name="count">How many values. Must not be negative.</param>
/// <returns>start, start + 1, ...</returns>
/// <example>LocalFunctions.Range(5, 3) yields 5, 6, 7.</example>
public static IEnumerable<int> Range(int start, int count)
{
// Runs at call time, not at first MoveNext, because this method has no yield.
ArgumentOutOfRangeException.ThrowIfNegative(count);
return Iterate();
IEnumerable<int> Iterate()
{
// i counts values produced. Invariant: start .. start + i - 1 are done.
for (int i = 0; i < count; i++) yield return start + i;
}
}
/// <summary>A generic static local function. Static means it captures nothing.</summary>
/// <returns>The larger of two pairs, compared by local helper.</returns>
/// <example>LocalFunctions.GenericLocal() returns "b 9".</example>
public static string GenericLocal()
{
// "a"/"b" and 4/9 are sample values. 9 wins, so the answer is "b 9".
return $"{Max("a", "b")} {Max(4, 9)}";
static T Max<T>(T x, T y) where T : IComparable<T> => x.CompareTo(y) >= 0 ? x : y;
}
}
list.Select(MyLocal) allocates like a lambda.var f = (int x) => x * 2; now compiles. The compiler infers Func<int, int>. Method groups get one too: var g = Twice;.var pick = object (bool b) => b ? 1 : "one"; fixes the return type when inference cannot.[Description("x")] (int x) => x puts the attribute on the generated method. ASP.NET minimal APIs read these.Func<…>.using System.ComponentModel;
using System.Reflection;
public static class NaturalTypes
{
/// <summary>A plain method, used as a method group.</summary>
/// <param name="x">Input.</param>
/// <returns>x times 2.</returns>
/// <example>NaturalTypes.Twice(4) returns 8.</example>
public static int Twice(int x) => 2 * x; // 2: the doubling factor
/// <summary>Shows inferred delegate types for a lambda and a method group.</summary>
/// <returns>The two inferred type names.</returns>
/// <example>NaturalTypes.InferredTypes() returns "Func`2 Func`2".</example>
public static string InferredTypes()
{
var square = (int x) => x * x; // inferred Func<int, int>
var twice = Twice; // works because Twice has exactly one overload
return $"{square.GetType().Name} {twice.GetType().Name}";
}
/// <summary>An explicit return type lets two branches return different types.</summary>
/// <returns>The results for true and false.</returns>
/// <example>NaturalTypes.ExplicitReturn() returns "1 one".</example>
public static string ExplicitReturn()
{
var pick = object (bool b) => b ? 1 : "one"; // 1 and "one" are sample values
return $"{pick(true)} {pick(false)}";
}
/// <summary>Reads an attribute placed on a lambda.</summary>
/// <returns>The Description text found on the lambda's method.</returns>
/// <example>NaturalTypes.LambdaAttribute() returns "doubles a number".</example>
public static string LambdaAttribute()
{
var f = [Description("doubles a number")] (int x) => x * 2; // 2: doubling factor
return f.Method.GetCustomAttribute<DescriptionAttribute>()!.Description;
}
}
var f = x => x; still fails.var p = int.Parse; fails because there are several overloads.var f = (string s) => s.Length; get? A: Func<string, int>. If no Func or Action fits, such as with ref parameters, the compiler makes an anonymous delegate type.Lambda parameters can now have default values and a params array, just like methods: var inc = (int x, int by = 1) => x + by;. The natural type cannot be Func, so the compiler makes an anonymous delegate type that keeps the default.
public static class DefaultLambdaParams
{
/// <summary>Calls a lambda with and without its optional argument.</summary>
/// <returns>Both results.</returns>
/// <example>DefaultLambdaParams.Demo() returns "6 15".</example>
public static string Demo()
{
var inc = (int x, int by = 1) => x + by; // by = 1: step by one unless told otherwise
return $"{inc(5)} {inc(5, 10)}"; // 5 and 10 are sample inputs
}
/// <summary>A params array on a lambda.</summary>
/// <returns>The sum of the arguments passed loosely.</returns>
/// <example>DefaultLambdaParams.ParamsDemo() returns 6.</example>
public static int ParamsDemo()
{
var sum = (params int[] xs) => xs.Sum();
return sum(1, 2, 3); // 1, 2, 3 are sample values
}
/// <summary>Shows the natural type is an anonymous delegate, not Func.</summary>
/// <returns>True when the inferred type is not a Func.</returns>
/// <example>DefaultLambdaParams.IsAnonymousDelegate() returns true.</example>
public static bool IsAnonymousDelegate()
{
var inc = (int x, int by = 1) => x + by; // same default step as above
return !inc.GetType().Name.StartsWith("Func");
}
}
Func drops the default. Func<int, int, int> f = (int x, int by = 1) => x + by; warns, and callers must pass both arguments.Func? A: Func's Invoke has no default values. The compiler makes a new delegate type whose Invoke carries the default.params used to work only with arrays. C# 13 allows any collection type that supports collection expressions. That includes Span<T>, ReadOnlySpan<T>, List<T>, IEnumerable<T>, and IReadOnlyList<T>.
params ReadOnlySpan<T> avoids the hidden array allocation on every call. The compiler builds the arguments in an inline array on the stack.params IEnumerable<T> lets callers pass loose values or an existing sequence with one overload.string.Join, Path.Combine, Task.WhenAll, and more.public static class ParamsCollections
{
/// <summary>Sums loose arguments with no heap array.</summary>
/// <param name="values">Arguments, built on the stack by the compiler.</param>
/// <returns>The total.</returns>
/// <example>ParamsCollections.Sum(1, 2, 3) returns 6.</example>
public static int Sum(params ReadOnlySpan<int> values)
{
int total = 0; // 0 is the empty sum
foreach (int v in values) total += v;
return total;
}
/// <summary>Joins loose values or any sequence.</summary>
/// <param name="parts">Strings to join.</param>
/// <returns>The parts joined by dashes.</returns>
/// <example>ParamsCollections.Dash("a", "b") returns "a-b".</example>
public static string Dash(params IEnumerable<string> parts) => string.Join("-", parts);
/// <summary>Calls each overload in the loose and the collection form.</summary>
/// <returns>A summary of all the results.</returns>
/// <example>ParamsCollections.Demo() returns "6 0 a-b x-y".</example>
public static string Demo()
{
int a = Sum(1, 2, 3); // loose ints, sample values
int none = Sum(); // empty span, so 0
string b = Dash("a", "b");
string c = Dash(new List<string> { "x", "y" }); // an existing sequence works too
return $"{a} {none} {b} {c}";
}
}
params ReadOnlySpan<T> overload next to an array one changes which is called. That is source compatible but can change behavior if they differ.await. Copy it out with ToArray() if you must keep it.params ReadOnlySpan<int> faster than params int[]? A: The array version allocates a new array on each call. The span version uses stack space the compiler sets up.Before C# 14, using ref, out, in, or scoped on a lambda parameter forced you to write every type. C# 14 lets you keep the modifier and drop the types. The delegate supplies them.
TryParser<int> p = (string text, out int result) => ...;TryParser<int> p = (text, out result) => ...;TryXxx delegates and ref-based callbacks./// <summary>A callback that changes a value in place.</summary>
public delegate void RefAction<T>(ref T value);
public static class TypelessModifiers
{
/// <summary>Uses out and ref on lambda parameters with no types written.</summary>
/// <returns>Parse result, parsed value, and the doubled variable.</returns>
/// <example>TypelessModifiers.Demo() returns "True 42 10".</example>
public static string Demo()
{
// C# 14: out with no type. Before, this needed (string text, out int result).
TryParser<int> parse = (text, out result) => int.TryParse(text, out result);
bool ok = parse("42", out int n); // "42" is a sample number
// ref with no type: the delegate says value is an int.
RefAction<int> twice = (ref value) => value *= 2; // * 2: doubles in place
int x = 5; // 5 is a sample start value
twice(ref x);
return $"{ok} {n} {x}";
}
/// <summary>Applies a ref callback to every element in place.</summary>
/// <param name="items">The array. It is changed.</param>
/// <param name="f">The in-place change.</param>
/// <returns>The same array.</returns>
/// <example>TypelessModifiers.ForEachRef([1, 2], (ref v) => v++) returns [2, 3].</example>
public static int[] ForEachRef(int[] items, RefAction<int> f)
{
// i walks the array. Invariant: items[0..i-1] have been changed.
for (int i = 0; i < items.Length; i++) f(ref items[i]);
return items;
}
}
var p = (text, out result) => ...; fails. There is nothing to infer the types from.params is the exception. A params lambda parameter still needs its type.ref, out, in, and scoped no longer force explicit types. The target delegate supplies the types.An event is a delegate field with restricted access. Outside code may only += and -=. Only the owning class may invoke it or set it to null. The standard shape is event EventHandler<TArgs>, raised from a protected virtual OnXxx method.
event keyword stops subscribers from wiping each other out with = or from raising the event themselves.(sender, args) shape is what designers, tools, and other developers expect./// <summary>Data for a price change.</summary>
public sealed class PriceChangedEventArgs(decimal oldPrice, decimal newPrice) : EventArgs
{
public decimal OldPrice { get; } = oldPrice;
public decimal NewPrice { get; } = newPrice;
}
/// <summary>A stock that raises an event when its price changes.</summary>
public class Stock(string symbol)
{
private decimal _price;
public string Symbol { get; } = symbol;
public event EventHandler<PriceChangedEventArgs>? PriceChanged;
public decimal Price
{
get => _price;
set
{
if (value == _price) return; // no change, no event
var args = new PriceChangedEventArgs(_price, value);
_price = value;
OnPriceChanged(args);
}
}
// ?.Invoke reads the field once, so a handler leaving on another thread is safe.
protected virtual void OnPriceChanged(PriceChangedEventArgs e) => PriceChanged?.Invoke(this, e);
}
public static class EventsDemo
{
/// <summary>Subscribes, changes the price twice, unsubscribes, changes it again.</summary>
/// <returns>The log of changes the handler saw.</returns>
/// <example>EventsDemo.Run() returns ["ACME 0->10", "ACME 10->12"].</example>
public static List<string> Run()
{
var log = new List<string>();
var stock = new Stock("ACME");
EventHandler<PriceChangedEventArgs> handler = (sender, e) =>
log.Add($"{((Stock)sender!).Symbol} {e.OldPrice}->{e.NewPrice}");
stock.PriceChanged += handler;
stock.Price = 10; // 10 and 12 are sample prices
stock.Price = 12;
stock.Price = 12; // same value, so no event
stock.PriceChanged -= handler; // the same delegate object, so removal works
stock.Price = 20; // nobody listening now
return log;
}
}
if (E != null) E(this, e); can throw if the last handler leaves between the check and the call. E?.Invoke(this, e) reads once.-= causes the leak in the next section.event add over a public delegate field? A: Outside code may only add and remove handlers. It cannot invoke the delegate or replace the whole list.OnXxx method? A: One place to raise the event. Subclasses can override it to react or change behavior.Subscribing stores a delegate in the publisher. The delegate's Target is the subscriber. So the publisher keeps the subscriber alive. If the publisher lives long, such as a static or app-wide service, every forgotten subscriber leaks.
A weak event stores the subscriber through a WeakReference. The GC may then collect the subscriber, and the publisher drops dead entries when it next raises.
Reading the figure. Solid red arrows are strong references. The GC follows them, so the closed view stays alive as long as the publisher does. The dashed green arrow is a weak reference that the GC does not follow. Notice the leak is not in the view at all. It is in the long-lived publisher holding the list.
+= with -= in Dispose.WeakEventManager. In plain .NET you write a small one like the sample.using System.Reflection;
using System.Runtime.CompilerServices;
/// <summary>A minimal weak event. Holds subscribers weakly and prunes dead ones.</summary>
public sealed class WeakEvent<TArgs>
{
private readonly List<(WeakReference<object> Target, MethodInfo Method)> _subs = [];
/// <summary>Adds an instance-method handler without keeping its target alive.</summary>
/// <param name="handler">Must be an instance method, not a static or lambda.</param>
/// <example>weak.Subscribe(view.OnTick) keeps view collectable.</example>
public void Subscribe(EventHandler<TArgs> handler)
{
if (handler.Target is null) throw new ArgumentException("static handlers not supported");
_subs.Add((new WeakReference<object>(handler.Target), handler.Method));
}
/// <summary>Calls every live handler and drops the dead ones.</summary>
/// <param name="sender">The publisher.</param>
/// <param name="args">Event data.</param>
/// <returns>How many handlers ran.</returns>
/// <example>weak.Raise(this, 5) returns 1 when one subscriber is alive.</example>
public int Raise(object sender, TArgs args)
{
int delivered = 0; // 0 handlers run so far
// Walk backwards so RemoveAt does not shift entries not yet visited.
// - 1 because the last valid index is Count - 1. Stop after index 0.
for (int i = _subs.Count - 1; i >= 0; i--)
{
if (_subs[i].Target.TryGetTarget(out object? target))
{
_subs[i].Method.Invoke(target, [sender, args]);
delivered++;
}
else
{
_subs.RemoveAt(i); // the subscriber was collected
}
}
return delivered;
}
public int Count => _subs.Count;
}
/// <summary>A long-lived publisher with a normal, strong event.</summary>
public sealed class Ticker
{
public event EventHandler<int>? Tick;
public WeakEvent<int> WeakTick { get; } = new();
public void Fire(int n)
{
Tick?.Invoke(this, n);
WeakTick.Raise(this, n);
}
}
/// <summary>A subscriber that counts ticks.</summary>
public sealed class TickView
{
public int Seen;
public void OnTick(object? sender, int n) => Seen += n;
}
public static class LeakDemo
{
/// <summary>Checks whether a subscriber survives a full GC.</summary>
/// <param name="weak">True to subscribe weakly, false for a normal event.</param>
/// <returns>True when the subscriber is still alive, which means it leaked.</returns>
/// <example>LeakDemo.SubscriberSurvivesGc(false) returns true.</example>
public static bool SubscriberSurvivesGc(bool weak)
{
var ticker = new Ticker();
WeakReference probe = Subscribe(ticker, weak);
// Collect twice with a finalizer pass between, so the GC finishes its work.
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
bool alive = probe.IsAlive;
GC.KeepAlive(ticker); // the publisher must outlive the check
return alive;
}
// In its own method, so no local in the caller keeps the view alive.
[MethodImpl(MethodImplOptions.NoInlining)]
private static WeakReference Subscribe(Ticker ticker, bool weak)
{
var view = new TickView();
if (weak) ticker.WeakTick.Subscribe(view.OnTick);
else ticker.Tick += view.OnTick;
return new WeakReference(view);
}
/// <summary>A live weak subscriber still receives events.</summary>
/// <returns>The sum of ticks the view saw.</returns>
/// <example>LeakDemo.WeakStillDelivers() returns 5.</example>
public static int WeakStillDelivers()
{
var ticker = new Ticker();
var view = new TickView();
ticker.WeakTick.Subscribe(view.OnTick);
ticker.Fire(2); // 2 and 3 are sample tick sizes
ticker.Fire(3);
return view.Seen;
}
}
Invoke is slow. Real weak event managers build a typed open delegate once per method.Dispose. If that is not possible, use a weak event that holds subscribers through WeakReference.A function pointer, delegate*<int, int, int>, is a raw address of a static method. It has no target object, no invocation list, and no heap allocation. You take it with &Method and call it like a delegate. It needs an unsafe context. delegate* unmanaged points at native code for interop.
[UnmanagedCallersOnly].public static unsafe class FunctionPointers
{
private static int Add(int a, int b) => a + b;
private static int Mul(int a, int b) => a * b;
/// <summary>Picks an operation by symbol and calls it through a function pointer.</summary>
/// <param name="op">'+' or '*'.</param>
/// <param name="a">Left operand.</param>
/// <param name="b">Right operand.</param>
/// <returns>The result.</returns>
/// <example>FunctionPointers.Apply('*', 6, 7) returns 42.</example>
public static int Apply(char op, int a, int b)
{
delegate*<int, int, int> f = op switch
{
'+' => &Add,
'*' => &Mul,
_ => throw new ArgumentException($"unknown op {op}", nameof(op)),
};
return f(a, b);
}
/// <summary>A table of function pointers, indexed like an array.</summary>
/// <returns>Each table entry applied to 3 and 4.</returns>
/// <example>FunctionPointers.Table() returns [7, 12].</example>
public static List<int> Table()
{
// stackalloc of 2 pointer slots: one per operation. No heap at all.
delegate*<int, int, int>* table = stackalloc delegate*<int, int, int>[2];
table[0] = &Add; // slot 0 is +
table[1] = &Mul; // slot 1 is *
// 3 and 4 are sample operands.
return [table[0](3, 4), table[1](3, 4)];
}
}
delegate* unmanaged signature corrupts the stack instead of throwing.delegate* faster than a delegate? A: It is a bare address. A delegate call loads the target and method from a heap object and may walk a multicast list.A method group is a method name used without calling it, as in items.Select(Parse). The compiler turns it into a delegate. Whether that allocates depends on the case:
obj.Method: a new delegate each time, because it binds obj.x => Parse(x) over Parse to get caching. On C# 11 and later both are cached.public static class MethodGroups
{
/// <summary>A static method used as a method group.</summary>
/// <param name="x">Input.</param>
/// <returns>x + 1.</returns>
/// <example>MethodGroups.Inc(1) returns 2.</example>
public static int Inc(int x) => x + 1; // + 1: the increment
private sealed class Box(int v)
{
public int Get() => v;
}
/// <summary>Converts the same method group twice at one site and compares instances.</summary>
/// <returns>Whether the static and instance conversions reused one delegate.</returns>
/// <example>MethodGroups.Reuse() returns "static=True instance=False".</example>
public static string Reuse()
{
var statics = new List<Func<int, int>>();
var instances = new List<Func<int>>();
var box = new Box(5); // 5 is a sample stored value
// 2 passes, so each site converts twice.
for (int i = 0; i < 2; i++)
{
statics.Add(Inc); // C# 11+: cached, same object both times
instances.Add(box.Get); // binds box, so a new delegate each time
}
// [0] and [1] are the delegates from the first and second pass.
return $"static={ReferenceEquals(statics[0], statics[1])} "
+ $"instance={ReferenceEquals(instances[0], instances[1])}";
}
/// <summary>Measures heap bytes for converting each kind n times.</summary>
/// <param name="n">Number of conversions.</param>
/// <returns>True when instance groups allocate and static groups do not.</returns>
/// <example>MethodGroups.AllocationGap(1000) returns true.</example>
public static bool AllocationGap(int n)
{
var box = new Box(1); // 1 is a sample stored value
long sink = 0; // 0: running total to keep the calls alive
// Warm up both paths so JIT work is not counted.
sink += ConvertStatic(1) + ConvertInstance(box, 1); // 1 pass each
long before = GC.GetAllocatedBytesForCurrentThread();
sink += ConvertStatic(n);
long staticBytes = GC.GetAllocatedBytesForCurrentThread() - before;
before = GC.GetAllocatedBytesForCurrentThread();
sink += ConvertInstance(box, n);
long instanceBytes = GC.GetAllocatedBytesForCurrentThread() - before;
GC.KeepAlive(sink);
// 0 bytes for cached statics. Instance groups pay one delegate per pass.
return staticBytes == 0 && instanceBytes > 0;
}
private static long ConvertStatic(int n)
{
long total = 0; // 0 is the empty sum
for (int i = 0; i < n; i++)
{
Func<int, int> f = Inc; // cached since C# 11
total += f(i);
}
return total;
}
private static long ConvertInstance(Box box, int n)
{
long total = 0; // 0 is the empty sum
for (int i = 0; i < n; i++)
{
Func<int> f = box.Get; // new delegate every pass
total += f();
}
return total;
}
}
-=. Caching rules changed in C# 11 and may change again.[MemoryDiagnoser] before you optimise.list.Select(int.Parse) allocate a delegate each time? A: Before C# 11, yes. Since C# 11 static method groups are cached like non-capturing lambdas. Instance method groups still allocate.Func and closure display classes in the allocation list.for loop bug still exists. Copy the variable into the body.static lambdas and local functions avoid hidden closures. Local functions also allow recursion and generics.params (12), params spans (13), typeless ref and out (14).delegate* is a bare address for hot or native paths. Static method groups are cached since C# 11.The next page, LINQ and Iterators, is where delegates do most of their work. It covers deferred execution, yield, and what LINQ costs.