Delegates, Events & Lambdas
17 questions foundA lambda captures OUTER variables by REFERENCE (not by value at creation time), meaning it sees whatever the variable's value is at the moment the lambda actually EXECUTES — with modern C# (5.0+), each 'foreach' iteration variable is a fresh variable, avoiding the classic capture bug, but a 'for' loop's shared counter variable can still be captured unexpectedly if not handled carefully.
var actions = new List<Action>();
for (int i = 0; i < 3; i++) {
int captured = i; // must copy into a NEW local to capture correctly in a 'for' loop
actions.Add(() => Console.WriteLine(captured));
}
foreach (var action in actions) action(); // 0, 1, 2 (correct, thanks to the local copy)
Real-world example Debugging a classic closure bug where all queued callbacks unexpectedly print the SAME final loop value instead of their individually intended value.
Common follow-ups: Does this same capture pitfall still apply to C#'s modern 'foreach' loops, or only to 'for' loops?
Iterators & yield return
Func<T1,...,TResult> represents a method that returns a value (the LAST type parameter is always the return type); Action<T1,...> represents a method with NO return value (void); Predicate<T> is specifically a method taking one argument and returning bool, functionally equivalent to Func<T, bool> but used idiomatically in older/certain collection APIs like List<T>.Find().
Func<int, int, int> add = (a, b) => a + b;
Action<string> log = message => Console.WriteLine(message);
Predicate<int> isEven = n => n % 2 == 0;
List<int> numbers = new() { 1, 2, 3, 4 };
int firstEven = numbers.Find(isEven); // 2
Real-world example Choosing the right built-in delegate type for a method parameter instead of always declaring a custom delegate type.
Common follow-ups: Why does Predicate<T> still exist as a separate type when Func<T, bool> could express the same shape?
LINQ
A standard event subscription (+=) creates a STRONG reference from the publisher to the subscriber, meaning the subscriber can't be garbage collected as long as the publisher is alive and the subscription remains — the weak event pattern uses WeakReference (or a dedicated helper like WeakEventManager) to hold a WEAK reference to the subscriber instead, letting it be collected even if never explicitly unsubscribed.
public class WeakEventSubscription {
private readonly WeakReference<Action> _weakHandler;
public WeakEventSubscription(Action handler) { _weakHandler = new WeakReference<Action>(handler); }
public void Raise() {
if (_weakHandler.TryGetTarget(out var handler)) handler();
// if the target was collected, TryGetTarget returns false -- no leak, no crash
}
}
Real-world example Preventing a long-lived singleton service (like an app-wide event bus) from indefinitely keeping short-lived UI elements alive via forgotten event subscriptions.
Common follow-ups: Why do WPF and similar frameworks provide built-in weak event manager helpers rather than expecting every developer to implement this manually?
Memory & Garbage Collection
A lambda that captures nothing is compiled as a STATIC, cached method — created once and reused for every call, with zero allocation overhead per invocation. A lambda that captures outer variables requires the compiler to generate a hidden CLOSURE CLASS to hold those captured variables, allocated fresh (typically) each time the enclosing method runs, which has real allocation implications in hot paths.
// No capture: compiled as a cached static delegate, zero per-call allocation
Func<int, int> square = x => x * x;
// Captures 'multiplier': compiler generates a closure class instance per call
int multiplier = 3;
Func<int, int> multiply = x => x * multiplier;
Real-world example Understanding a subtle source of allocation pressure in a hot loop that repeatedly creates lambdas capturing local state.
Common follow-ups: How would you restructure code to avoid this closure allocation in a genuinely performance-critical hot path?
Memory & Garbage Collection
C# allows a method to be assigned to a delegate type even if the method's RETURN TYPE is more derived (covariance) than the delegate's declared return type, or its PARAMETER TYPES are less derived / more general (contravariance) than the delegate's declared parameter types — reflecting that a method returning something MORE specific, or accepting something MORE general, can always safely stand in.
public delegate object Factory(); // expects something returning 'object'
string CreateString() => "hello"; // returns a MORE derived type (string)
Factory factory = CreateString; // OK: covariant return type match
public delegate void Handler(string s);
void HandleObject(object o) => Console.WriteLine(o); // accepts a LESS derived type
Handler handler = HandleObject; // OK: contravariant parameter type match
Real-world example Assigning an existing utility method to a delegate type without needing an exact, literal signature match.
Common follow-ups: How does this delegate variance relate to the similar covariance/contravariance rules for generic interfaces like IEnumerable<out T>?
Generics
Since standard .NET events require a 'void'-returning delegate signature, an async event handler is FORCED to be 'async void' — this is the ONE legitimate, accepted use case for async void in C#, but you should still wrap the handler's body in a try/catch, since an unhandled exception in an async void method crashes the process rather than being awaitable/catchable by the caller.
button.Clicked += async (sender, e) => {
try {
await SaveDataAsync(); // legitimate async void usage: matches EventHandler's signature
} catch (Exception ex) {
Console.WriteLine($"Error in click handler: {ex.Message}"); // must catch here -- caller can't
}
};
Real-world example Handling a UI button click that needs to perform an asynchronous save operation, while still safely catching any resulting exceptions.
Common follow-ups: Why can't a caller catch an exception thrown from inside an 'async void' method the normal way, unlike an 'async Task' method?
Asynchronous Programming
A delegate is a type-safe reference to one or more methods matching a specific signature, functioning like a type-checked function pointer -- assigning a method to a delegate lets you invoke that method indirectly through the delegate variable, and a single delegate can even hold multiple method references (a multicast delegate), invoking each in turn -- C# also provides three general-purpose built-in delegate types (Func<T> for methods returning a value, Action<T> for void methods, and Predicate<T> for boolean tests) that cover most everyday use cases without needing a custom delegate declaration.
delegate void MessageDelegate(string message);
void PrintMessage(string msg) => Console.WriteLine(msg);
MessageDelegate del = PrintMessage;
del("Hello, delegates!"); // invokes PrintMessage indirectly
// Multicast: both methods run in sequence
del += msg => Console.WriteLine("Also: " + msg);
del("Hi again");
Real-world example A logging framework accepts a Action<string> delegate parameter so callers can plug in their own custom log-writing behavior (console, file, remote endpoint) without the framework itself needing to know or care about the specific destination.
Common follow-ups: How do events build on top of delegates to add publish/subscribe semantics?;What's the difference between Func<T,R> and a custom delegate with the identical signature?
Functional Interfaces & Method References;Design Patterns in C#