The => syntax you've been reading since lesson 099, finally explained properly — a lambda is just a nameless method, written inline, that becomes a delegate instance.
Look back at almost every code sample from the last three lessons. x => x * x. o => o.Total > 0. item => item.QuantityInStock == 0. You've already been reading and writing lambda expressions for a while — you just hadn't been told their name or exactly what the compiler does with them.
Time to fix that. Once lambdas click formally, the rest of this module — closures, local functions, higher-order functions — will make far more sense, because all of them build directly on this one syntax.
A lambda expression is a small, unnamed method you write inline, right at the spot where you need it — instead of declaring a separate named method somewhere else and referencing it. It's the fast, terse way to create a delegate instance.
A lambda expression has the general form (parameters) => body, and it always comes in one of two shapes:
x => x * xreturn keywordx => { var y = x * x; return y; }{ }return if it produces a valueBoth shapes compile down to the same thing: an actual method, generated by the compiler, wrapped in a delegate instance — exactly the kind of delegate instance you've been assigning to Action, Func, and Predicate variables all along.
Every custom delegate example in lesson 097 pointed at a proper, separately-declared method: static void PrintGreeting() { ... }, then Action greet = PrintGreeting;. That's fine for reusable, meaningful logic. But for a one-line check like "is this price over 100?", declaring a whole named method somewhere else, just to reference it once, is pure ceremony — and it physically separates the logic from the place that actually uses it, making the code harder to read top to bottom.
C# 2.0 tried to solve this with anonymous methods: delegate(int x) { return x * x; }. It worked, but it was still verbose, and parameter types had to be spelled out every time.
C# 3.0 introduced the lambda expression syntax you know today, alongside Func/Action-friendly type inference. x => x * x says exactly the same thing as the anonymous method above, in a fraction of the characters, with the parameter type inferred from context instead of spelled out. Lambdas became — and remain — the default, idiomatic way to write inline behavior in C#.
A lambda is not a different kind of thing from the delegates you already know — it's a syntax for creating a delegate instance. Every time you write x => x * x where a Func<int, int> is expected, the compiler is doing the same job it would do for Func<int, int> square = Square; with a named method — it's just building the method for you, on the spot, with no name.
All three rows produce a delegate instance with the exact same behavior. The syntax got shorter over twenty years; the underlying idea — "a method, referenced as a value" — never changed.
Func<int, int> square = x => x * x;
Func<int, int>, tells the compiler: one int parameter, returns int.int," x is known to be int without you writing it.x => x * x becomes a real (compiler-named) method with a real body.int result = square(5); // 25
using System;
class Program
{
static void Main()
{
// Expression-bodied — single expression, value returned automatically
Func<int, int> square = x => x * x;
// Zero parameters need empty parentheses
Action greet = () => Console.WriteLine("Hello!");
// Multiple parameters need parentheses around them
Func<int, int, int> add = (a, b) => a + b;
// Statement-bodied — braces and an explicit return
Func<int, int, int> addLoudly = (a, b) =>
{
int sum = a + b;
Console.WriteLine($"Adding {a} + {b}");
return sum;
};
Console.WriteLine(square(5)); // 25
greet(); // Hello!
Console.WriteLine(add(2, 3)); // 5
Console.WriteLine(addLoudly(4, 6)); // "Adding 4 + 6" then 10
}
}
Code → Meaning → Result: Notice the parentheses rules: zero parameters still need (), two or more parameters need () around them, but exactly one parameter can drop the parentheses entirely (x => ...). Every one of these four lambdas is target-typed by the variable it's assigned to — none of them spell out a parameter type.
Sorting a list of products by price, using List<T>.Sort's Comparison<T> overload — a delegate type shaped exactly like a lambda needs:
using System;
using System.Collections.Generic;
public record Product(string Name, decimal Price);
class Program
{
static void Main()
{
var products = new List<Product>
{
new("Keyboard", 79.99m),
new("Monitor", 249.99m),
new("Mouse", 24.99m),
};
// Comparison<Product> is `int Comparison<T>(T x, T y)` — a lambda fits perfectly.
products.Sort((a, b) => a.Price.CompareTo(b.Price));
foreach (var p in products)
Console.WriteLine($"{p.Name}: {p.Price:C}");
// Mouse: $24.99
// Keyboard: $79.99
// Monitor: $249.99
// Same idea, filtering — a preview of LINQ's Where, coming up soon.
var affordable = products.FindAll(p => p.Price < 100m);
Console.WriteLine($"Affordable items: {affordable.Count}");
}
}
No named comparer method, no named predicate method — the sort order and the filter condition live exactly where they're used, which is where a reader most wants to see them.
A named method is like a formal memo filed in a folder elsewhere in the building — proper, reusable, but you have to walk over and fetch it every time you need it. A lambda is a sticky note you write and stick directly onto the form that needs it — quick, disposable, and exactly where the reader's eyes already are. Both convey instructions; a lambda just skips the trip to the filing cabinet for instructions used once.
When the compiler sees a lambda, it generates a real method for it — usually as a private method on your class (given a compiler-generated name you'll never type, something like <Main>b__0_0), then wraps that method in a delegate instance, exactly like the Action greet = PrintGreeting; case from lesson 096. If the lambda references nothing from its surrounding scope, the compiler can even make that generated method static and cache a single delegate instance for reuse across calls — a small performance optimization. The moment a lambda does reference an outer variable, the compiler has to generate something more elaborate to keep that variable alive — which is exactly the mechanism the next lesson, Closures, explains in full.
One more form worth knowing exists, even though it's outside this lesson's scope: when a lambda is assigned to an Expression<Func<...>> instead of a plain Func<...>, the compiler doesn't generate executable IL at all — it builds a data structure describing the lambda's logic (an expression tree) instead. That's how tools like Entity Framework Core translate your C# lambda into SQL. It's an advanced topic you'll meet properly when you reach EF Core; for everyday delegate use, a lambda always compiles to a real method.
A lambda has no meaning by itself. x => x * x isn't a value you can inspect in isolation — it only means something once the compiler knows what delegate type it's being converted to. Lambdas are a syntax for producing delegate instances, not a competing mechanism.
Func<int,int> square = x => x * x; and static int Square(int x) => x * x; (a local function, covered two lessons from now) can look almost identical at a glance. The difference: a lambda is a delegate value the moment you write it; a local function is a plain method that only becomes a delegate value if you explicitly assign it to one. You'll see the full comparison in lesson 103.
Writing a, b => a + b — this doesn't compile; multiple parameters must be parenthesized.
(a, b) => a + b. Remember: exactly one parameter is the only case where parentheses are optional, and even then, (x) => x * x is also legal if you prefer consistency.
x => { x * x } — braces require a return statement; a bare expression inside braces doesn't implicitly return.
Either drop the braces entirely (x => x * x) or use return inside them (x => { return x * x; }).
var square = x => x * x; — this fails to compile. var gives the compiler no delegate type to infer x's type from.
Either give x an explicit type (var square = (int x) => x * x; — legal since C# 10) or declare the variable with an explicit delegate type, like Func<int, int> square = x => x * x;.
params => body = a method with no name, written exactly where you need it.(params) => body — for creating a delegate instance, nothing more exotic than that.return.You've been writing lambdas for three lessons already — let's make sure the "why" behind the syntax is solid.
1. In Func<int, int> square = x => x * x;, how does the compiler know that x is an int?
Correct: B
Why B is correct: This is target-typing — the lambda has no type of its own; the compiler reads the declared delegate type (Func<int, int>) and infers that its single parameter must be int.
Why A is incorrect: Variable names carry no type information to the compiler; x could be named anything.
Why C is incorrect: Lambda parameters have no default type — without a target type, the compiler cannot infer one at all (see Mistake 3 above).
Why D is incorrect: Type inference for lambdas happens entirely at compile time, not by executing anything.
Reinforcement: A lambda's parameter types always come from the delegate type context it's written in.
2. Which of these lambda expressions is written correctly?
Correct: B
Why B is correct: Two parameters require parentheses, and this is a valid expression-bodied lambda — the value of a + b is returned automatically.
Why A is incorrect: Multiple parameters must be wrapped in parentheses: (a, b), not bare a, b.
Why C is incorrect: Once braces are used, the lambda becomes statement-bodied and needs an explicit return — { a + b } alone doesn't compile.
Why D is incorrect: This is missing the parameter list and the => entirely — it isn't a lambda at all, just an (invalid) expression.
Reinforcement: Parenthesize multi-parameter lambdas; add braces and return only when you need multiple statements.
3. What does a lambda expression compile down to, in the typical case where it references only its own parameters?
Correct: B
Why B is correct: The compiler generates an actual method (with a compiler-chosen name) for the lambda's body, then creates a delegate instance pointing at it — the exact same mechanism used for a method group like Action greet = PrintGreeting;.
Why A is incorrect: There's nothing magical at the runtime level — a lambda is ordinary compiled IL, just generated by the compiler instead of typed by hand.
Why C is incorrect: Lambdas are compiled ahead of time, not interpreted from text at runtime — that's a different, much slower mechanism C# does not use here.
Why D is incorrect: There's no special inlining trick — it's a normal method call through a delegate, like any other delegate invocation from lesson 096.
Reinforcement: A lambda is compiler-generated syntax sugar over the exact same delegate machinery you already know.
4. Why does var square = x => x * x; fail to compile, while Func<int, int> square = x => x * x; works fine?
Correct: B
Why B is correct: Target-typing needs a known delegate type to infer x's type from. var alone provides none, so the compiler has nothing to infer against and rejects the code — unless the parameter type is spelled out explicitly, as in var square = (int x) => x * x;.
Why A is incorrect: var can be used with lambdas — but only when the parameter types are stated explicitly in the lambda itself.
Why C is incorrect: Lambdas convert to any compatible delegate type — Action, Predicate<T>, or a custom delegate — not only Func.
Why D is incorrect: x * x is a perfectly valid expression; the failure has nothing to do with the body.
Reinforcement: A lambda's parameter types must come from somewhere — either the target type or an explicit annotation — var alone supplies neither.
You now understand exactly what a lambda is and how it becomes a delegate. Next up: what happens when a lambda reaches outside itself and grabs a variable from its surroundings — closures.
dotnetmadeeasy.com — Learn C# and .NET, the right way.