Delegates, Events & Lambdas
17 questions found
How does variable capture (closures) work in a C# lambda expression, and what pitfall occurs when capturing a loop variable?
Intermediate
A 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
How do the built-in Func<>, Action<>, and Predicate<T> generic delegate types differ, and when would you use each?
Advanced
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
How would you implement the 'weak event pattern' to prevent event subscribers from causing a memory leak by keeping the publisher alive indefinitely?
Advanced
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
How does a lambda expression get compiled differently when it captures NO outer variables versus when it DOES capture outer state?
Advanced
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
How do delegate covariance and contravariance work with method group conversions in C#?
Advanced
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
How would you implement a custom EventHandler using async/await, avoiding the common 'async void' anti-pattern while still matching the standard EventHandler delegate signature?
Advanced
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
What is a delegate in C#, and how does it enable passing methods as values?
Beginner
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#