Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

C# & .NET (C# 14, .NET 10 LTS)  Module 12 – Delegates, lambdas, events and functional style

Lambdas and closures

Write C# lambdas from one-line bodies to natural types, and predict exactly what a closure captures, in for and foreach loops alike, with runs that prove it.

  • Intermediate
  • 20 minutes
  • Examples run with .NET 10.0.401 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 8 of 20 minutes free
Free: A lambda is a method without a name, written where it is used · A closure captures the variable, not its value. With a pass: 2 in-depth parts and 3 interview answers, about 12 minutes.

What you will learn

  • Write expression and statement lambdas, and tell which delegate type a lambda gets with var
  • Predict what a closure captures, and when the lambdas made in a loop share one variable
  • Explain what a captured variable costs in allocations and how long it keeps objects in memory
  • Use static lambdas to stop accidental captures, and use the lambda features of C# 9 to C# 14

Before you start

What you’ll learn · Before you start

What you will learn

  • Write expression and statement lambdas, and tell which delegate type a lambda gets with var
  • Predict what a closure captures, and when the lambdas made in a loop share one variable
  • Explain what a captured variable costs in allocations and how long it keeps objects in memory
  • Use static lambdas to stop accidental captures, and use the lambda features of C# 9 to C# 14

Before you start

On this page

A booking screen makes four seat buttons in a for loop, one for each seat from B2-11 to B2-14, and gives each a lambda that announces its seat. All four announce “no seat at index 4”. The lambdas are right; what they captured is not.

You will write lambdas in their usual forms, see which delegate type the compiler gives each one, and predict exactly what a closure captures, in loops and outside them.

A lambda is a method without a name, written where it is used

A lambda expression: An unnamed method written where a delegate is expected, such as x => x * 2: parameters, the => operator and a body that is one expression or a block of statements. is parameters, the => operator and a body. forms.cs writes the common shapes and asks the compiler which delegate type each one became:

forms.cs: lambda forms and their natural types C# · forms/forms.cs
// Lambdas in their common forms. Each one becomes a delegate; with var, the compiler chooses the delegate type.
using System.Globalization;

Func<int, int> square = x => x * x;              // an expression lambda: its body is one expression
Action<string> greet = name =>                    // a statement lambda: a block of statements
{
    string text = $"Namaste, {name}";
    Console.WriteLine(text);
};
Func<int, int, bool> divides = (d, n) => n % d == 0;   // two parameters need parentheses
Func<string> today = () => "Saturday";                  // no parameters: empty parentheses

Console.WriteLine($"square(12) = {square(12)}");
greet("Asha");
Console.WriteLine($"3 divides 12: {divides(3, 12)}, today: {today()}");

// A natural type (C# 10): with var and typed parameters, the compiler infers Func<...> or Action<...>.
var parse = (string s) => int.Parse(s, CultureInfo.InvariantCulture);
var shout = (string s) => Console.WriteLine(s.ToUpperInvariant());
Console.WriteLine($"parse is a {Name(parse.GetType())}; shout is an {Name(shout.GetType())}");
shout("platform 4");

// An explicit return type, for branches that have no type in common.
var status = object (bool paid) => paid ? 1 : "pending";
Console.WriteLine($"status is a {Name(status.GetType())}: {status(true)}, {status(false)}");

// A method group with exactly one overload has a natural type as well.
var isBlank = string.IsNullOrWhiteSpace;
Console.WriteLine($"isBlank is a {Name(isBlank.GetType())}: {isBlank("   ")}");

// The type names as C# writes them, for printing.
static string Name(Type type)
{
    string name = type.Name switch
    {
        "Int32" => "int",
        "String" => "string",
        "Boolean" => "bool",
        "Object" => "object",
        _ => type.Name,
    };
    if (!type.IsGenericType)
    {
        return name;
    }

    string bare = name[..name.IndexOf('`')];
    return $"{bare}<{string.Join(", ", type.GetGenericArguments().Select(Name))}>";
}

Output

square(12) = 144
Namaste, Asha
3 divides 12: True, today: Saturday
parse is a Func<string, int>; shout is an Action<string>
PLATFORM 4
status is a Func<bool, object>: 1, pending
isBlank is a Func<string, bool>: True

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run forms.cs

x => x * x has a single expression as its body, and that expression is the result. The greet lambda has a block of statements in braces, like a method body. One parameter needs no parentheses; none, or two and more, do. A lambda converts to any delegate type its shape fits, Func<int, int> for square and Action<string> for greet.

With var there is no delegate type to fit, so since C# 10 the compiler infers one, the lambda’s natural type: The delegate type the compiler infers for a lambda or a method group when nothing else gives one, as with var: a Func or an Action when one fits, otherwise a delegate type the compiler writes.: Func<string, int> for parse, Action<string> for shout. It picks Func or Action when one fits and otherwise writes a delegate type of its own. An explicit return type, object (bool paid) => …, settles branches that share no type, and a method group with one overload gets a natural type too. A lambda whose parameters have no types gives nothing to infer from:

nonatural/Program.cs C# · nonatural/Program.cs
// A lambda with untyped parameters, assigned to var, gives the compiler nothing to infer a delegate type from.
var parse = s => int.Parse(s);

Console.WriteLine(parse("42"));
Building nonatural Python · nonatural_session.py
from session import run

# nonatural assigns a lambda whose parameter has no type to var. dotnet run builds the project first and stops at the
# error.
run("""
cd nonatural
!dotnet run
""")

Output

$ cd nonatural
$ dotnet run
nonatural/Program.cs(2,13): error CS8917: The delegate type could not be inferred. [nonatural/App.csproj]

The build failed. Fix the build errors and run again.
[exit status 1]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 nonatural_session.py

In short, what var gives each form:

You write var gives it
(string s) => int.Parse(s) Func<string, int>
(string s) => Console.WriteLine(s) Action<string>
object (bool paid) => paid ? 1 : "pending" Func<bool, object>
s => int.Parse(s) nothing: error CS8917

Lambdas take more than this. Parameters you ignore can be discards, (_, _) => 42. Since C# 12 a lambda can declare default values and params, and since C# 14 a modifier such as out needs no type. A static lambda refuses to capture anything at all; an API can still hand it the data it needs as an argument.

Checkpoint

Check: Lambda forms and natural types

  1. Question 1 of 2 Which of these declarations compiles?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: var twice = (int x) => x * 2;

    With var, the compiler infers a natural type from the lambda's parameter types and its result. Give the parameters types, and an explicit return type when the branches differ.

  2. Question 2 of 2 Which of these is a statement lambda?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: name => { string text = $"Namaste, {name}"; Console.WriteLine(text); }

    An expression lambda has one expression as its body; a statement lambda has a block in braces and uses return when it has a result.

A closure captures the variable, not its value

A lambda may use the local variables and parameters of the method around it. Together with the variables it uses, it is a closure: A lambda or local function together with the outer variables it uses. It keeps those variables, not copies of their values, so it sees later changes and keeps the variables alive as long as the delegate., and closures keep the variables themselves, not copies. capture.cs shows what that means:

capture.cs: a lambda reads, changes and keeps variables C# · capture/capture.cs
// A lambda that uses a local variable of the method around it captures the variable itself, not a copy of its value.
int limit = 5;
Func<int, bool> isLow = units => units < limit;
Console.WriteLine($"limit {limit}: 4 units low? {isLow(4)}");

limit = 3;                                   // the lambda reads the variable when it runs, so it sees the change
Console.WriteLine($"limit {limit}: 4 units low? {isLow(4)}");

// A lambda can change a captured variable, and the method sees the change.
int alerts = 0;
Action<string> raise = sku =>
{
    alerts++;
    Console.WriteLine($"  alert {alerts}: {sku} is low");
};
raise("TEA-250");
raise("RICE-5");
Console.WriteLine($"alerts after two calls: {alerts}");

// Each call of MakeCounter makes a new variable, so each counter has its own.
Func<int> first = MakeCounter();
Func<int> second = MakeCounter();
Console.WriteLine($"first: {first()}, {first()}, {first()}; second: {second()}");

static Func<int> MakeCounter()
{
    int count = 0;
    return () => ++count;
}

Output

limit 5: 4 units low? True
limit 3: 4 units low? False
  alert 1: TEA-250 is low
  alert 2: RICE-5 is low
alerts after two calls: 2
first: 1, 2, 3; second: 1

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run capture.cs

isLow read limit when it ran, not when it was made, so after limit = 3 it said False. raise changed alerts, and the program saw 2. Each call of MakeCounter made a new count, so the counters counted apart. Step through the first part:

Interactive · step by step One variable, shared by the method and the lambda Step through the first five lines of capture.cs. Watch the one limit that both the program and isLow use.
One variable, shared by the method and the lambda, step 5 of 5. Showing The program, isLow, limit (holds 3), isLow(4) (returned False). In focus: isLow(4), limit.reads and writesreads when it runsThe programint limit = 5;isLowunits => units < limitlimitholds 3isLow(4)returned Falseread

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. The variable. int limit = 5 declares a local variable of the program. Because a lambda below uses it, the compiler keeps it in a place that both can reach.
  2. The lambda is made. isLow = units => units < limit makes a delegate. It does not copy 5: it refers to the variable limit itself.
  3. The first call reads 5. isLow(4) runs the body now, reads limit, finds 5, and 4 < 5 is True: the first line of the output.
  4. The program changes limit. limit = 3 writes the same variable. Nothing tells the lambda, and nothing needs to: it has no copy to update.
  5. The same call reads 3. isLow(4) again reads limit when it runs and finds 3, so 4 < 3 is False: the second line. A closure captures the variable, not the value it had.

A captured variable: A local variable, parameter or this of the code around a lambda that the lambda uses. The method and the lambda share the one variable, so a change by either is seen by both. is shared by the method and every lambda that uses it, and that breaks the seat buttons. A for loop declares its variable once, in its initializer, so every lambda made in the loop captures the same i, which holds the end value when the lambdas run. A foreach loop declares its variable inside the loop, a new one for each pass, a placement the C# specification says decides how lambdas capture it. Before C# 5, foreach shared one variable too.

loops.cs: lambdas made in for and foreach loops C# · loops/loops.cs
// Lambdas made inside loops. A for loop has one variable for all its passes; foreach has a new one in every pass.
var fromFor = new List<Func<int>>();
for (int i = 0; i < 3; i++)
{
    fromFor.Add(() => i);
}

Console.WriteLine($"for:            {string.Join(" ", fromFor.Select(f => f()))}");

var fromForeach = new List<Func<int>>();
foreach (int n in new[] { 0, 1, 2 })
{
    fromForeach.Add(() => n);
}

Console.WriteLine($"foreach:        {string.Join(" ", fromForeach.Select(f => f()))}");

// The fix for the for loop: copy the variable into a local declared inside the loop body.
var fixedFor = new List<Func<int>>();
for (int i = 0; i < 3; i++)
{
    int copy = i;
    fixedFor.Add(() => copy);
}

Console.WriteLine($"for with copy:  {string.Join(" ", fixedFor.Select(f => f()))}");

// The same bug with seats: every lambda reads the one variable, which ended at 4 after the loop.
string[] seats = ["B2-11", "B2-12", "B2-13", "B2-14"];
var announce = new List<Func<string>>();
for (int s = 0; s < seats.Length; s++)
{
    announce.Add(() => s < seats.Length ? seats[s] : $"no seat at index {s}");
}

Console.WriteLine($"seats from for: {string.Join(", ", announce.Select(a => a()))}");

Output

for:            3 3 3
foreach:        0 1 2
for with copy:  0 1 2
seats from for: no seat at index 4, no seat at index 4, no seat at index 4, no seat at index 4

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run loops.cs

Capturing a for loop's variable

for (int s = 0; s < seats.Length; s++) announce.Add(() => seats[s]); gives every lambda the one s, which is 4 after the loop, so each would read past the end of the array. Declare int seat = s; inside the loop and capture seat, or loop with foreach.

The seat lambdas of loops.cs guard the index, so they said “no seat at index 4” four times instead of throwing. The three loops of the program, side by side:

Loop Variables the loop makes What three lambdas return
for (int i = 0; i < 3; i++) one i, 3 when the loop ends 3 3 3
foreach (int n in …) one n per pass 0 1 2
for with int copy = i; inside one copy per pass 0 1 2

Each closure is also an object the compiler makes for you, so capturing costs memory, and the captured variables live as long as the delegate.

Key idea

Closures capture variables, not values, and read them when they run: a for loop’s single variable gives every lambda its last value, while foreach and a copy declared in the loop body give each pass its own.

Checkpoint

Check: What a closure captures, in loops and outside them

  1. Question 1 of 2 for (int i = 1; i <= 3; i++) fs.Add(() => i * 10); then each lambda in fs is called after the loop. What do they return?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: 40 40 40

    The lambdas read i when they run. The for loop's single variable has moved on to 4 by then, so every lambda returns 40.

  2. Question 2 of 2 int count = 0; Action add = () => count += 5; add(); add(); count = count * 2; add(); What is count now?

    Type a number.

    A hint

    The lambda and the program change the same variable.

    Show the answer: Question 2 of 2

    Answer: 25

    Two calls make count 10, the program doubles it to 20, and the third call adds 5: 25. There is one count, used by both.

Inside a closure: the class the compiler writes, what it costs and what it keeps alive Pro: opens with a Pro, Premium or Ultimate pass

A closure is an object that the compiler writes for you. It costs memory when it is made, and it holds everything it captured for as long as a delegate refers to it. The runs below measure both.

In this part:

  • The class that holds captured variables, read back by reflection
  • A kept lambda keeps its whole closure alive, reproduced

1 diagram · 2 runnable examples (C#) · about 6 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

static lambdas, state arguments and the lambda features of C# 9 to C# 14 Pro: opens with a Pro, Premium or Ultimate pass

A static lambda promises to capture nothing, and the compiler holds it to that promise. With an API that hands the lambda the value it needs, the promise costs nothing, and the measured saving is 88 bytes a call.

In this part:

  • static turns an accidental capture into a build error
  • Passing state instead of capturing it, measured
  • Discards, natural types, defaults, params and modifiers, release by release
  • A default value belongs to the delegate type, not to the lambda

1 diagram · 6 runnable examples (C#, Python) · about 7 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Practise: counters and a memo

counters.cs checks three helpers built on closures: a counter, a counter that starts where it is told, and Memoize, which remembers each result of a slow fare lookup. All eight checks fail on the starter:

counters.cs: eight checks of counters and a memo C# · practice/counters.cs
// Practice: write Counters.Start, Counters.From and Memo.Memoize, then run this file until all eight checks pass.
// What each method must do is in the comment above it. The fares are made up for this exercise.
(string Check, Func<string> Run, string Expected)[] checks =
[
    ("a counter counts from 1", () => Calls(Counters.Start(), 3), "1 2 3"),
    ("two counters are independent", TwoCounters, "1 2 3 | 1 2"),
    ("From starts where it is told", () => Calls(Counters.From(10), 3), "10 11 12"),
    ("a counter keeps counting between uses", KeepsCounting, "1 2, then 3"),
    ("Memoize returns the function's results", () => MemoFares(["Pune", "Nashik", "Pune"]).Results, "350 220 350"),
    ("Memoize calls the function once per input", () => MemoFares(["Pune", "Nashik", "Pune", "Pune"]).Lookups, "2 lookups"),
    ("two memoized functions keep separate results", SeparateMemos, "350 and 700"),
    ("a failed call is not remembered", FailureNotRemembered, "throws, then 220"),
];

int passed = 0;
foreach (var (check, run, expected) in checks)
{
    string got;
    try
    {
        got = run();
    }
    catch (Exception e)
    {
        got = $"throws {e.GetType().Name}";
    }

    bool ok = got == expected;
    if (ok)
    {
        passed++;
    }

    Console.WriteLine(ok ? $"{check}: ok" : $"{check}: FAIL, {got} instead of {expected}");
}

Console.WriteLine($"{passed} of {checks.Length} checks pass");
return passed == checks.Length ? 0 : 1;

static string Calls(Func<int> counter, int times) =>
    string.Join(" ", Enumerable.Range(0, times).Select(_ => counter()));

static string TwoCounters()
{
    Func<int> a = Counters.Start();
    Func<int> b = Counters.Start();
    string first = Calls(a, 3);
    return $"{first} | {Calls(b, 2)}";
}

static string KeepsCounting()
{
    Func<int> c = Counters.Start();
    string first = Calls(c, 2);
    return $"{first}, then {c()}";
}

// A slow fare lookup that counts how often it really runs.
static (string Results, string Lookups) MemoFares(string[] cities)
{
    int lookups = 0;
    Func<string, int> fare = city =>
    {
        lookups++;
        return city == "Pune" ? 350 : 220;
    };
    Func<string, int> fast = Memo.Memoize(fare);
    string results = string.Join(" ", cities.Select(c => fast(c)));
    return (results, $"{lookups} lookups");
}

static string SeparateMemos()
{
    Func<string, int> sleeper = Memo.Memoize<string, int>(_ => 350);
    Func<string, int> ac = Memo.Memoize<string, int>(_ => 700);
    return $"{sleeper("Pune")} and {ac("Pune")}";
}

static string FailureNotRemembered()
{
    bool down = true;
    Func<string, int> fast = Memo.Memoize<string, int>(city => down ? throw new TimeoutException("fares service down") : 220);
    string first;
    try
    {
        first = fast("Nashik").ToString();
    }
    catch (TimeoutException)
    {
        first = "throws";
    }

    down = false;
    return $"{first}, then {fast("Nashik")}";
}

static class Counters
{
    // Returns a counter: each call returns the next number, 1, 2, 3 and so on. Every counter has its own count.
    public static Func<int> Start() => throw new NotImplementedException();

    // Returns a counter whose first call returns first, then first + 1, and so on.
    public static Func<int> From(int first) => throw new NotImplementedException();
}

static class Memo
{
    // Returns a function that gives the same results as f, but calls f only once for each input it has seen, and
    // remembers the result in a Dictionary<TIn, TOut> that only this function uses. When f throws, nothing is
    // remembered, so the next call with that input calls f again.
    public static Func<TIn, TOut> Memoize<TIn, TOut>(Func<TIn, TOut> f)
        where TIn : notnull => throw new NotImplementedException();
}

Output (exit status 1)

a counter counts from 1: FAIL, throws NotImplementedException instead of 1 2 3
two counters are independent: FAIL, throws NotImplementedException instead of 1 2 3 | 1 2
From starts where it is told: FAIL, throws NotImplementedException instead of 10 11 12
a counter keeps counting between uses: FAIL, throws NotImplementedException instead of 1 2, then 3
Memoize returns the function's results: FAIL, throws NotImplementedException instead of 350 220 350
Memoize calls the function once per input: FAIL, throws NotImplementedException instead of 2 lookups
two memoized functions keep separate results: FAIL, throws NotImplementedException instead of 350 and 700
a failed call is not remembered: FAIL, throws NotImplementedException instead of throws, then 220
0 of 8 checks pass

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run counters.cs

Write the counters and Memoize

Copy counters.cs. In From, declare a local that starts at first and return a lambda that returns it and adds one; Start is From(1). In Memoize, make a Dictionary<TIn, TOut> and return a lambda that looks the input up, calls f only when it is missing and stores the result after f returns. Run dotnet run counters.cs until it prints 8 of 8 checks pass.

Then check yourself on lambda forms and captures:

Practice that adapts to you

Practise this lesson

6 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 6 A statement lambda is assigned to a Func<int, int>. How does it give its result?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: With a return statement in its block

    Expression lambdas return their expression. Statement lambdas are blocks and use return, like any method.

  2. Question 2 of 6 var pick = (bool card) => card ? 1 : "cash"; does not compile. Which line fixes it and keeps var?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: var pick = object (bool card) => card ? 1 : "cash";

    Since C# 10 a lambda may declare its return type before its parameter list; the parameters must then be in parentheses.

  3. Question 3 of 6 Count takes a Func<string, int>. Does var len = (string s) => s.Length; Count(len); compile?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: Yes: len's natural type is Func<string, int>, the type Count asks for

    A natural type is an ordinary delegate type, Func or Action when one fits, so a lambda kept in var can be passed to any method that takes that type.

  4. Question 4 of 6 first = MakeCounter(); second = MakeCounter(); then first(), first(), second(), first(). What does the last call return?

    Type a number.

    Show the answer: Question 4 of 6

    Answer: 3

    Each call of MakeCounter declares a new count, so the counters count apart. first was called twice before, so its third call returns 3; second's single call does not affect it.

  5. Question 5 of 6 Which loop gives three lambdas the values 0, 1 and 2?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: foreach (int n in new[] { 0, 1, 2 }) fs.Add(() => n);

    A lambda keeps its own value only when the variable it captures is new in each pass: foreach, or a copy that the lambda itself uses.

  6. Question 6 of 6 A method starts a timer whose lambda prints status, a local. Right after starting it, the method sets status = "sent" and returns. The timer fires a second later. What does it print?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: sent: the lambda reads the variable when it runs

    Capture by variable reaches across time: whatever runs the lambda later sees the variable as it is then.

Interview questions

Fresher · What is a closure in C#?

A closure is a lambda or a local function together with the outer variables it uses: locals and parameters of the method around it, or this. It holds on to the variables, not to snapshots of what they held: it sees later changes, its own changes are seen by the method, and the variables live as long as the delegate does. In a for loop every lambda shares the one loop variable and sees its last value; foreach gives each pass a new variable. To fix a value at the moment the lambda is made, copy it into a local declared where the lambda is made.

  1. Fresher to mid

    A screen builds 4 seat buttons in a for loop and gives each a click lambda that books seats[i]. Every click throws IndexOutOfRangeException. Explain why, fix it, and say why the same code written with foreach works.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A dashboard keeps a small Func<string> title callback for every report it has shown. Memory grows by about 8 MB per report, although the callbacks only return titles, and a memory snapshot shows the report arrays reachable from the callbacks. Explain how, and fix it.

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    A team proposes a rule: every lambda on a hot path must be static. What does static prevent and what does it not, how do you pass a static lambda the data it needs, and would you adopt the rule?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A lambda is parameters, => and a body that is one expression or a block; with var it gets a natural type, Func or Action when one fits, and untyped parameters give it none.
  • A closure captures variables, not values: a lambda reads them when it runs and can change them for the method too.
  • A for loop has one variable for all its passes, so its lambdas share the last value; foreach and a copy declared in the loop body give each pass its own.

Terms in this lesson

captured variable
A local variable, parameter or this of the code around a lambda that the lambda uses. The method and the lambda share the one variable, so a change by either is seen by both.
closure
A lambda or local function together with the outer variables it uses. It keeps those variables, not copies of their values, so it sees later changes and keeps the variables alive as long as the delegate.
lambda expression
An unnamed method written where a delegate is expected, such as x => x * 2: parameters, the => operator and a body that is one expression or a block of statements.
natural type
The delegate type the compiler infers for a lambda or a method group when nothing else gives one, as with var: a Func or an Action when one fits, otherwise a delegate type the compiler writes.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.