← Open in the full interactive course (progress tracking, search & more)

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.

What Is It?

The Simple Explanation

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.

The Technical Definition

A lambda expression has the general form (parameters) => body, and it always comes in one of two shapes:

➡ Expression-Bodied

Statement-Bodied

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

Why Does It Exist?

The Problem — Tiny Logic Shouldn't Need a Named Method Far Away

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.

The Solution — A Terse, Inline Syntax

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

The key insight

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.

Big Picture

Approach Example Era
Named method groupFunc<int,int> f = Square;Since delegates existed (C# 1.0)
Anonymous methodFunc<int,int> f = delegate(int x) { return x * x; };C# 2.0 (2005)
Lambda expressionFunc<int,int> f = x => x * x;C# 3.0 (2007) — still the default today

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.

How It Works

WRITING AND USING A LAMBDA
1. THE COMPILER ALREADY KNOWS THE TARGET SHAPE
Func<int, int> square = x => x * x;
2. PARAMETER TYPES ARE INFERRED — NOT GUESSED, DERIVED
3. THE COMPILER GENERATES AN ACTUAL METHOD
4. INVOKE IT LIKE ANY OTHER DELEGATE
int result = square(5); // 25

Simple Example

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.

Real-World Example

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.

Analogy

A Sticky Note vs a Filed Memo

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.

Under the Hood

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.

Common Confusion

1. "Lambdas are a separate feature from delegates" — they're not

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.

2. Lambda vs local function — they look alike, but aren't the same thing

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.

Common Mistakes

Mistake 1 — Forgetting parentheses for zero or multiple parameters

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.

Mistake 2 — Mixing expression and statement syntax

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; }).

Mistake 3 — Expecting a lambda to work with no target type at all

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

When Should I Use It?

Mental Model

params => body = a method with no name, written exactly where you need it.
The target delegate type tells the compiler what the parameter types must be — a lambda never invents its own shape.
Every lambda you've ever written became a real method and a real delegate instance behind the scenes.

Key Takeaway


Check Your Understanding

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?

Show answer

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?

Show answer

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?

Show answer

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?

Show answer

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.