Lambda expressions and extension methods behind LINQ
Every query operator is an extension method taking a delegate, and knowing the difference between a delegate and an expression tree explains IQueryable.
Delegates and lambdas
// A lambda with an expression body compiles to a method plus a delegate
Func<int, bool> isEven = n => n % 2 == 0;
// A statement body is the same delegate with more lines
Func<int, bool> isBig = n =>
{
var doubled = n * 2;
return doubled > 100;
};
// Func<T, TResult> returns a value; Action<T> returns nothing
Action<string> log = message => Console.WriteLine(message);
// Predicate<T> is the old name for Func<T, bool>
Predicate<int> legacy = n => n > 0;
// The compiler infers the parameter types from the target type
var lengths = new[] { "a", "bb", "ccc" }.Select(s => s.Length);// A closure captures the variable, not its value
var counter = 0;
Func<int> next = () => ++counter; // counter lives on a compiler-generated class
Console.WriteLine(next()); // 1
Console.WriteLine(next()); // 2
// The classic loop-capture bug
var actions = new List<Action>();
for (var i = 0; i < 3; i++)
{
actions.Add(() => Console.WriteLine(i)); // captures the variable i
}
foreach (var a in actions) a(); // prints 3, 3, 3
// Fixed by copying into a fresh variable per iteration
actions.Clear();
for (var i = 0; i < 3; i++)
{
var captured = i;
actions.Add(() => Console.WriteLine(captured)); // prints 0, 1, 2
}foreachhas had per-iteration capture semantics since C# 5, so the bug above only affectsforloops andwhileloops.- A captured variable is hoisted into a generated class, which means a closure allocates. A lambda that captures nothing can be cached by the compiler as a static delegate.
- Capturing
thisis easy to do accidentally inside a method, and it keeps the whole object alive for as long as the delegate lives.
Extension methods and iterators
// An extension method is a static method with a this parameter in a static class
public static class EnumerableExtensions
{
public static IEnumerable<T> WhereNotNull<T>(this IEnumerable<T?> source)
where T : class
{
ArgumentNullException.ThrowIfNull(source);
foreach (var item in source)
if (item is not null) yield return item;
}
// Deferred: nothing runs until the result is enumerated
public static IEnumerable<T> TakeEvery<T>(this IEnumerable<T> source, int n)
{
ArgumentNullException.ThrowIfNull(source);
if (n <= 0) throw new ArgumentOutOfRangeException(nameof(n));
var index = 0;
foreach (var item in source)
if (index++ % n == 0) yield return item;
}
// Eager: materialise deliberately when the source may change underneath you
public static IReadOnlyList<T> ToStableList<T>(this IEnumerable<T> source) =>
Array.AsReadOnly(source.ToArray());
}
// Compose operators; the query is a pipeline description, not a set of steps
var names = people
.WhereNotNull()
.Where(p => p.IsActive)
.TakeEvery(2)
.Select(p => p.Name)
.ToStableList();- An iterator method does not execute until someone enumerates it. That is why a validation exception thrown inside one appears at the
foreach, not at the call. - Argument validation in an iterator method runs late for the same reason. Extract the body into a non-iterator method that validates first, then returns the iterator.
yield returncompiles into a state machine class, so an iterator allocates once per enumeration, which matters in a tight loop.IEnumerable<T>can be enumerated more than once with different results; the type gives you no guarantee of stability.
💡
Deferred execution is the single most important behaviour to internalise. A query that looks like a value is actually a description of work, and it will re-run every time you enumerate it — including inside a logging statement or a debugger watch.
FAQ
Why does my iterator method not throw where I expected?
Because none of the body runs until the first MoveNext. Validate arguments in a wrapper method that is not itself an iterator, then return the iterator from it.
Should I use a lambda or a local method?
A local method when it does not capture anything, because it compiles to a static method with no delegate allocation. A lambda when you need to pass it as a delegate or read the call site inline.
Related
Query syntax and method syntax Deferred execution and querying a database
Last refreshed 2026-09-18.