Dataslope logoDataslope

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.

Code Block
C# 13

The built-in delegate types you'll use 99% of the time:

TypeShape
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.

Code Block
C# 13

Two forms of body are allowed:

  • Expression bodyx => expr — implicitly returns the value of expr.
  • Statement bodyx => { ... } — needs an explicit return if 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.

Code Block
C# 13

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.

Code Block
C# 13

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.

Code Block
C# 13

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)); // 14

Why 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.

Code Block
C# 13

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.

QuestionSelect one

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>.

On this page