Lambdas and Delegates
How C# represents functions as values — and the subtle but important differences between lambdas, delegates, methods, and expression trees
We've been writing lambdas like n => n * 2 for several pages
without explaining the machinery underneath them. This page lifts the
hood. By the end of it you'll understand what a delegate is, what a
lambda compiles to, what a closure captures, and why
Expression<Func<T,bool>> is fundamentally different from
Func<T,bool>.
This is the most "language mechanics" page in the course. It is worth the patience — every page after this assumes you can read a lambda and know what it's doing.
Delegates: the type of a function
A delegate is a type that represents a function with a specific
signature. Just like int is the type of integers, Func<int, int> is the type of functions from int to int.
The built-in delegate types you'll use 99% of the time:
| Type | Shape |
|---|---|
Action | () => void |
Action<T> | (T) => void |
Action<T1, T2> | (T1, T2) => void |
Func<TResult> | () => TResult |
Func<T, TResult> | (T) => TResult |
Func<T1, T2, TResult> | (T1, T2) => TResult |
Predicate<T> | (T) => bool |
Action is for void-returning functions (side effects). Func is
for value-returning functions. Predicate<T> is a niche older alias
for Func<T, bool> — LINQ uses Func<T, bool>, not Predicate<T>.
Lambda syntax
A lambda is an inline literal of a delegate type. It is written
parameters => body.
Two forms of body are allowed:
- Expression body —
x => expr— implicitly returns the value ofexpr. - Statement body —
x => { ... }— needs an explicitreturnif it has a return type.
Almost everywhere in LINQ you'll see the expression form.
Method references
If a function you want already exists as a named method, you can pass the method directly — no need to wrap it in a lambda.
Prefer the method reference — nums.Select(Double) — when the
lambda would just call the method. It reads better.
Closures: capturing variables
A lambda can use variables from its enclosing scope. The compiler generates a hidden class to hold those captured variables. This is called a closure.
The lambda captured the variable, not the value of threshold at
the time of capture. This is occasionally surprising; usually it is
what you want.
The classic loop-variable bug
The most famous closure bug in C# is capturing a loop variable.
If you encounter older codebases, you may see explicit
int copy = i; inside loops to dodge the older capture behavior.
Modern C# fixed this in the foreach/for of loop variables,
but the lesson stands: prefer capturing immutable data.
When a lambda becomes data: expression trees
This is the deepest idea on this page. Most of the time a lambda compiles to executable code — a regular method the runtime can call. But there is a second form: a lambda can compile to a data structure describing itself.
That data structure is called an expression tree, and its type
is Expression<Func<...>>.
using System;
using System.Linq.Expressions;
// Compiled to executable code — you can call it.
Func<int, int> doubleFunc = x => x * 2;
Console.WriteLine(doubleFunc(7)); // 14
// Compiled to a data structure — you can inspect it.
Expression<Func<int, int>> doubleExpr = x => x * 2;
Console.WriteLine($"Type: {doubleExpr.GetType().Name}");
Console.WriteLine($"Parameter: {doubleExpr.Parameters[0].Name}");
Console.WriteLine($"Body: {doubleExpr.Body}");
Console.WriteLine($"Body kind: {doubleExpr.Body.NodeType}");
// You can still compile and call it if you want.
var compiled = doubleExpr.Compile();
Console.WriteLine(compiled(7)); // 14Why does this matter? Because LINQ providers — EF Core, MongoDB
LINQ, etc. — accept Expression<Func<T,bool>>. They don't call
your lambda; they walk its tree and translate it into another
language (SQL, BSON, an HTTP query).
We're not going to write a LINQ provider in this course, but it is useful to know the difference exists. When you start using EF Core in real work, you'll occasionally hit "the LINQ provider could not translate this expression" errors — and now you'll know why.
Functions as values: a practical example
Putting lambdas and HOFs together — a small "rule engine" defined entirely as data.
The rules are data. You could load them from a config file, combine them dynamically, or build a UI to toggle them — all without recompiling. That power comes from treating functions as values.
What is the difference between Func<int, bool> and Expression<Func<int, bool>>?
They are the same; Expression<...> is just a longer alias.
Func<int, bool> can only be created from a method, while Expression<Func<int, bool>> can be created from a lambda.
Func<int, bool> compiles to executable code that can be invoked directly; Expression<Func<int, bool>> compiles to a data structure that describes the lambda and can be inspected or translated by a LINQ provider.
Expression<Func<int, bool>> is the async version of Func<int, bool>.