This is the third post in the series. Generics was a class with a blank for a type. Delegates was a variable that holds a method. A lambda expression is the last piece of that picture: it is the method itself, written inline, without a name. If you understood the delegates post, this one is short, because a lambda is not a new concept. It is a shorthand for something you already know.
The problem lambdas solve
At the end of the delegates post we passed behaviour into methods. Here is what that looks like with our books. The repository:
public class BookRepository
{
public List<Book> GetBooks()
{
return new List<Book>
{
new Book { Id = 1, Title = "The Great Gatsby", Author = "F. Scott Fitzgerald", Price = 10 },
new Book { Id = 2, Title = "To Kill a Mockingbird", Author = "Harper Lee", Price = 12 },
new Book { Id = 3, Title = "1984", Author = "George Orwell", Price = 15 },
new Book { Id = 4, Title = "Pride and Prejudice", Author = "Jane Austen", Price = 8 },
new Book { Id = 5, Title = "The Catcher in the Rye", Author = "J.D. Salinger", Price = 11 }
};
}
}
public class Book
{
public int Id { get; set; }
public string Title { get; set; }
public string Author { get; set; }
public int Price { get; set; }
}
Now I want the books that cost more than 10. List<T> has a FindAll method that takes a delegate, so the delegates way is to write a method that matches the shape and pass it in:
static bool IsExpensive(Book book)
{
return book.Price > 10;
}
var books = new BookRepository().GetBooks();
var expensiveBooks = books.FindAll(IsExpensive); // pass the method, no brackets
It works. But look at the ceremony. Four lines of method declaration, a name, a return type, braces, all wrapping one line of actual logic: book.Price > 10. And I will probably never call IsExpensive from anywhere else. Naming and declaring a full method for one throwaway line of logic is the problem lambdas remove.
The lambda version
A lambda expression keeps the logic and deletes everything else:
var books = new BookRepository().GetBooks();
var expensiveBooks = books.FindAll(x => x.Price > 10);
foreach (var book in expensiveBooks)
{
Console.WriteLine($"Id: {book.Id}, Title: {book.Title}, Author: {book.Author}, Price: {book.Price}");
}
That prints To Kill a Mockingbird, 1984, and The Catcher in the Rye. The entire IsExpensive method became eight characters of logic inside the call.
Read x => x.Price > 10 piece by piece:
xis the parameter, one book at a time, the same asBook bookwas inIsExpensive=>is the lambda arrow, read it as “goes to”x.Price > 10is the body, the return value
So the whole thing reads “x goes to x.Price greater than 10”, meaning: given a book x, return whether its price is over 10. The compiler works out that x is a Book from the list you called FindAll on, and that the result is a bool from the expression. All the ceremony you deleted is inferred.
One naming note, because it bit me while writing this. My first version of this code called the variable cheapBooks while filtering Price > 10, which selects the expensive ones. The compiler will never catch a lie in a variable name. Lambdas make code short, and short code makes wrong names stand out more, not less.
A lambda is a delegate in disguise
Here is the connection to the previous post, and it is the single most important thing to understand about lambdas. A lambda has no type of its own. It has to be stored in a delegate, because a lambda is just a compact way of writing the method the delegate holds.
FindAll accepts a Predicate<Book>, which is a built-in delegate for methods that take a Book and return a bool, a sibling of the Func and Action types from the delegates post. You can make the connection explicit by storing the lambda in a delegate variable yourself:
Func<Book, bool> isExpensive = x => x.Price > 10; // the lambda fills the delegate
Console.WriteLine(isExpensive(books[2])); // True, 1984 costs 15
Same power button idea as before. The lambda is the button, the delegate variable stores it, the brackets press it. Everything from the delegates post applies unchanged, because nothing new is happening. Lambdas are the syntax, delegates are the type.
The shape flexes with the parameters:
() => Console.WriteLine("no parameters") // Action
(a, b) => a + b // Func<int, int, int>
x => // statement body with braces
{
var discounted = x.Price - 2;
return discounted > 10;
}
With a single parameter the brackets around it are optional, which is why x => x.Price > 10 has none. With braces you write full statements and an explicit return.
Closures, the lambda that remembers
A lambda can use variables from the method it sits inside:
var maxPrice = 12;
var affordable = books.FindAll(x => x.Price <= maxPrice);
maxPrice is not a parameter of the lambda, it is captured from the surrounding scope. This is called a closure. It looks obvious, but there is a detail interviews love: the lambda captures the variable, not its value at the time you wrote it. If maxPrice changes before the lambda runs, the lambda sees the new value. Most of the time closures just quietly work, and they are what make patterns like WithRetry(() => LoadBooksFromApi(url)) from the delegates post possible, because the lambda carries url along with it.
The same idea inside Meta, Google and Netflix
Like the previous two posts, this concept is everywhere once you know its shape. The arrow syntax has effectively conquered every major language.
Meta (React, JavaScript). JavaScript calls them arrow functions, and React codebases are built out of them:
const names = users.map(u => u.name);
const active = users.filter(u => u.isActive);
Same arrow, same “parameter goes to expression” reading, filling the same role: a tiny function handed to a method that runs it for every item.
Google (Kotlin, Android). Kotlin made lambdas so central that Android UI code is mostly trailing lambdas:
val expensive = books.filter { it.price > 10 }
buyButton.setOnClickListener { checkout() }
Kotlin even skips declaring the parameter, it is the implicit x. The click listener is the delegates post and this post in one line: a slot on the button, filled with an inline method.
Netflix (Java). Java added lambdas in 2014 because the streams API needed them, and RxJava, which Netflix built its client architecture on, is unusable without them:
books.stream().filter(b -> b.getPrice() > 10).toList();
A thinner arrow, the identical idea.
Python (Instagram, YouTube). Python spells the keyword out, which is where the name comes from:
expensive = sorted(books, key=lambda b: b.price)
The takeaway matches the series: x => x.Price > 10, u => u.name, { it.price > 10 }, b -> b.getPrice() > 10, lambda b: b.price. Different companies, different languages, one concept. A method without a name, written where it is needed.
Summary
- A lambda expression is a method without a name, written inline where a delegate is expected.
- Read
=>as “goes to”: parameters on the left, the body on the right. - A lambda has no type of its own. It is stored in a delegate, usually
Func,Action, orPredicate. Lambdas are the syntax, delegates are the type. - Parameter types and the return type are inferred by the compiler from the delegate the lambda fills.
- A closure is a lambda that captures a variable from its surrounding scope, and it captures the variable, not a snapshot of its value.
- Arrow functions in JavaScript, Kotlin trailing lambdas, Java streams, and Python lambdas are the same idea in other languages.
Interview questions and answers
1. What is a lambda expression in C#?
A compact way to write a method inline, without a name, access modifier, or explicit types. It is used wherever a delegate is expected, so the logic can be written at the point it is passed instead of declared as a separate method.
2. How do you read the => operator?
As “goes to”. The left side lists the parameters, the right side is the body. x => x.Price > 10 reads “x goes to x.Price greater than 10”, a method that takes x and returns that comparison.
3. How do lambdas relate to delegates?
A lambda is the method, a delegate is the type that stores it. A lambda expression has no type of its own and must be assigned to a delegate type such as Func, Action, or Predicate. Lambdas are the syntax, delegates are the type.
4. What is the difference between Func, Action, and Predicate?
All three are built-in generic delegates that lambdas commonly fill. Action returns void, Func returns a value with the last generic type as the return type, and Predicate<T> takes one T and returns bool, which is what List<T>.FindAll uses.
5. What is the difference between an expression lambda and a statement lambda?
An expression lambda has a single expression as its body and returns it implicitly, like x => x.Price > 10. A statement lambda has braces, can contain multiple statements, and needs an explicit return if it returns a value.
6. What is a closure?
A lambda that captures a variable from the scope it was created in. The lambda keeps access to that variable even when it runs later or somewhere else. It captures the variable itself, not its value at creation time, so if the variable changes before the lambda runs, the lambda sees the new value.
7. How does the compiler know the type of x in x => x.Price > 10?
From the delegate the lambda is filling. FindAll on a List<Book> takes a Predicate<Book>, so the compiler infers x is a Book and checks that the body returns a bool. This is type inference against the target delegate.
8. Can you assign a lambda to var?
Since C# 10, yes, if the compiler can work out a natural type, which needs explicit parameter types: var isExpensive = (Book x) => x.Price > 10; becomes a Func<Book, bool>. Before C# 10 a lambda could only be assigned to an explicit delegate or expression tree type.
9. What is the difference between Func<T, bool> and Expression<Func<T, bool>>?
Func<T, bool> is a delegate holding compiled, runnable code. Expression<Func<T, bool>> is an expression tree, a data structure describing the code, which a library can inspect and translate. This is how EF Core works: Where on an IQueryable takes an expression tree and translates your lambda into SQL instead of running it in memory.
10. When would you use a named method instead of a lambda?
When the logic is reused in more than one place, needs its own unit tests, or is long enough that a name documents it better than inline code. Lambdas are for short, single-use logic at the point of the call. If the lambda grows past a few lines, promote it to a method and pass the method name, which works because both fill the same delegate.