0
0
C Sharp (C#)programming~15 mins

Lambda with captures (closures) in C Sharp (C#) - Deep Dive

Choose your learning style9 modes available
Overview - Lambda with captures (closures)
What is it?
A lambda with captures, also called a closure, is a small function you write inside your code that can remember and use variables from the place where it was created, even if that place is no longer running. In C#, lambdas are anonymous functions that can capture variables from their surrounding scope. This means the lambda keeps a reference to those variables and can use or change them later when the lambda runs. Closures let you write flexible and powerful code by bundling behavior with data.
Why it matters
Without closures, you would have to pass all data explicitly every time you call a function, making your code longer and harder to manage. Closures let you keep related data and behavior together, which makes your programs easier to write, read, and maintain. They are especially useful for things like event handlers, callbacks, and deferred execution. Without closures, many modern programming patterns and libraries would be much more complicated or impossible.
Where it fits
Before learning about lambda captures, you should understand basic C# syntax, variables, and how functions work. After mastering closures, you can explore advanced topics like asynchronous programming, LINQ queries, and functional programming patterns that rely heavily on lambdas and closures.
Mental Model
Core Idea
A lambda with captures is like a tiny function that carries its own little backpack of variables from where it was born, so it can use them anytime later.
Think of it like...
Imagine you write a recipe on a card and tuck it into your backpack along with the special ingredients you need. Even if you go somewhere else, you still have both the recipe and the ingredients together, ready to cook. The lambda is the recipe, and the captured variables are the ingredients in the backpack.
┌───────────────┐
│ Outer Function│
│  Variables    │
│  x = 5        │
│  y = 10       │
└──────┬────────┘
       │
       ▼
┌─────────────────────────┐
│ Lambda (Closure)         │
│ Uses x and y from above  │
│ Can run later, remembers │
│ values even if outer is  │
│ done                    │
└─────────────────────────┘
Build-Up - 7 Steps
1
FoundationUnderstanding Lambdas in C#
🤔
Concept: Learn what a lambda expression is and how to write one.
In C#, a lambda is a short way to write a function without a name. For example: Func square = x => x * x; This means 'square' is a function that takes a number x and returns x times x. You can call it like this: int result = square(4); // result is 16
Result
You create a simple function quickly and call it to get the expected output.
Understanding lambdas as anonymous functions is the first step to seeing how they can capture variables around them.
2
FoundationVariables and Scope Basics
🤔
Concept: Learn how variables exist in different parts of your code and when they are accessible.
Variables declared inside a function or block are only available there. For example: int x = 10; { int y = 20; // x and y are accessible here } // y is NOT accessible here, only x is This is called 'scope'.
Result
You understand that variables have limited visibility depending on where they are declared.
Knowing variable scope helps you see why capturing variables in lambdas is special—it lets the lambda reach outside its immediate scope.
3
IntermediateCapturing Variables in Lambdas
🤔Before reading on: do you think a lambda can use variables declared outside its own body? Commit to yes or no.
Concept: Learn that lambdas can use variables from their surrounding scope, creating closures.
Consider this code: int x = 5; Func addX = y => x + y; Here, the lambda 'addX' uses 'x' from outside. Even if 'x' changes later, the lambda remembers it. Console.WriteLine(addX(3)); // prints 8 x = 10; Console.WriteLine(addX(3)); // prints 13
Result
The lambda uses the current value of 'x' when called, showing it captures 'x' by reference.
Understanding that lambdas capture variables by reference explains why changes to those variables affect the lambda's behavior.
4
IntermediateHow Closures Keep Variables Alive
🤔Before reading on: do you think variables captured by lambdas are copied or kept alive? Commit to your answer.
Concept: Learn that captured variables live as long as the lambda does, even if the original scope ends.
Example: Func makeCounter() { int count = 0; return () => ++count; } var counter = makeCounter(); Console.WriteLine(counter()); // 1 Console.WriteLine(counter()); // 2 Here, 'count' stays alive inside the lambda even after makeCounter() finishes.
Result
The lambda remembers and updates 'count' each time it runs, showing closure keeps variables alive.
Knowing closures extend variable lifetime helps you write functions that maintain state without global variables.
5
IntermediateCapturing Multiple Variables and Mutability
🤔Before reading on: if a lambda captures multiple variables, can it change them all? Commit to yes or no.
Concept: Learn that lambdas can capture several variables and modify them if they are not readonly.
Example: int a = 1, b = 2; Action update = () => { a += 10; b += 20; }; update(); Console.WriteLine(a); // 11 Console.WriteLine(b); // 22
Result
The lambda changes both 'a' and 'b', showing captured variables can be mutable.
Understanding mutability in captured variables is key to avoiding bugs with unexpected state changes.
6
AdvancedClosures and Variable Capture Pitfalls
🤔Before reading on: do you think a loop variable captured in a lambda behaves as expected? Commit to yes or no.
Concept: Learn about common mistakes when capturing loop variables in lambdas and how to fix them.
Example of a common pitfall: List> funcs = new List>(); for (int i = 0; i < 3; i++) { funcs.Add(() => i); } foreach (var f in funcs) Console.WriteLine(f()); This prints 3, 3, 3 instead of 0, 1, 2. Fix: for (int i = 0; i < 3; i++) { int copy = i; funcs.Add(() => copy); }
Result
The fixed code prints 0, 1, 2 as expected.
Knowing how loop variables are captured prevents subtle bugs in asynchronous or deferred code.
7
ExpertCompiler and Runtime Behind Closures
🤔Before reading on: do you think the compiler creates a hidden class to implement closures? Commit to yes or no.
Concept: Learn how C# compiler transforms lambdas with captures into hidden classes that hold variables and methods.
When you write a lambda that captures variables, the compiler creates a special hidden class called a closure class. This class has fields for the captured variables and a method for the lambda body. The lambda becomes a method of this class, and an instance of the class holds the variables alive. This is why captured variables live beyond their original scope.
Result
Understanding this explains why closures keep variables alive and how lambdas are implemented under the hood.
Knowing the compiler's transformation helps you understand performance and lifetime implications of closures.
Under the Hood
The C# compiler converts lambdas with captured variables into special hidden classes called closure classes. These classes have fields for each captured variable and a method representing the lambda's code. When the lambda is created, an instance of this class is made, holding the current state of captured variables. This instance lives as long as the lambda delegate does, keeping variables alive beyond their original scope. When the lambda runs, it executes the method on this instance, accessing or modifying the captured variables through the fields.
Why designed this way?
This design allows lambdas to behave like regular functions while still remembering their environment. It avoids copying variables unnecessarily and supports mutable captured variables. Alternatives like copying values would break expected behavior, especially when variables change after lambda creation. The closure class approach balances flexibility, performance, and language simplicity.
┌─────────────────────────────┐
│ Source Code with Lambda      │
│ int x = 5;                  │
│ Func<int> f = () => x + 1;  │
└─────────────┬───────────────┘
              │
              ▼
┌─────────────────────────────┐
│ Compiler generates:          │
│ class ClosureClass {         │
│   public int x;             │
│   public int Lambda() {      │
│     return x + 1;           │
│   }                         │
│ }                           │
│                             │
│ var closure = new ClosureClass();
│ closure.x = 5;               │
│ Func<int> f = closure.Lambda;
└─────────────────────────────┘
Myth Busters - 4 Common Misconceptions
Quick: Does a lambda capture variables by value or by reference? Commit to your answer.
Common Belief:Lambdas capture variables by copying their current value at the time of creation.
Tap to reveal reality
Reality:Lambdas capture variables by reference, meaning they refer to the original variable, not a copy.
Why it matters:Believing lambdas capture by value leads to confusion when variables change after lambda creation, causing unexpected results.
Quick: If a variable goes out of scope, is it immediately destroyed even if a lambda captured it? Commit to yes or no.
Common Belief:Captured variables are destroyed when their original scope ends, so lambdas can't use them afterward.
Tap to reveal reality
Reality:Captured variables live as long as the lambda delegate referencing them exists, extending their lifetime beyond the original scope.
Why it matters:Misunderstanding variable lifetime can cause bugs or memory leaks if you don't realize closures keep variables alive.
Quick: Does each lambda in a loop capture a unique copy of the loop variable by default? Commit to yes or no.
Common Belief:Each lambda in a loop automatically captures a separate copy of the loop variable.
Tap to reveal reality
Reality:All lambdas capture the same loop variable, leading to all lambdas seeing the final loop value unless you create a local copy inside the loop.
Why it matters:This misconception causes common bugs where lambdas behave identically instead of differently as expected.
Quick: Are captured variables always immutable inside lambdas? Commit to yes or no.
Common Belief:Captured variables cannot be changed inside lambdas; they are read-only.
Tap to reveal reality
Reality:Captured variables can be modified inside lambdas if they are not declared readonly or const.
Why it matters:Assuming immutability can prevent you from using closures effectively for stateful behavior.
Expert Zone
1
Captured variables are stored as fields in a compiler-generated class, which means lambdas can cause subtle memory retention if not handled carefully.
2
Closures can capture variables from multiple nested scopes, not just the immediate one, creating a chain of references.
3
The compiler optimizes some closures by reusing closure classes or avoiding allocations when possible, but this depends on the lambda's complexity.
When NOT to use
Avoid using closures when performance is critical and you want to minimize allocations, such as in tight loops or real-time systems. Instead, use explicit classes or structs to hold state. Also, avoid capturing large objects unintentionally, which can cause memory leaks. For simple callbacks without state, prefer static methods or lambdas without captures.
Production Patterns
Closures are widely used in event handlers, LINQ queries, asynchronous programming with async/await, and dependency injection. Professionals use closures to encapsulate state in callbacks, create factory methods, and implement functional patterns like currying and partial application. Understanding closure internals helps optimize memory usage and avoid common bugs in complex applications.
Connections
Functional Programming
Closures are a fundamental concept in functional programming languages and paradigms.
Knowing how closures work in C# helps understand pure functions, immutability, and higher-order functions common in functional programming.
Memory Management
Closures affect variable lifetime and memory allocation in managed environments.
Understanding closures deepens knowledge of garbage collection and object lifetime, helping prevent memory leaks.
Psychology of Memory
Closures conceptually resemble how human memory stores context and associations.
Recognizing that closures 'remember' context like human memory aids in grasping their purpose and behavior.
Common Pitfalls
#1Capturing loop variables directly causing unexpected results.
Wrong approach:List> funcs = new List>(); for (int i = 0; i < 3; i++) { funcs.Add(() => i); } // All funcs return 3 instead of 0,1,2
Correct approach:List> funcs = new List>(); for (int i = 0; i < 3; i++) { int copy = i; funcs.Add(() => copy); } // funcs return 0,1,2 as expected
Root cause:The loop variable 'i' is captured by reference, so all lambdas share the same variable which changes during the loop.
#2Assuming captured variables are copied and immutable inside lambdas.
Wrong approach:int x = 5; Func f = () => x; x = 10; Console.WriteLine(f()); // expecting 5 but prints 10
Correct approach:Understand that lambdas capture by reference, so changes to 'x' affect the lambda's output.
Root cause:Misunderstanding capture semantics leads to wrong expectations about lambda behavior.
#3Unintentionally capturing large objects causing memory leaks.
Wrong approach:var bigObject = new LargeObject(); Func f = () => bigObject.ToString(); // bigObject stays alive as long as f does
Correct approach:Avoid capturing large objects or nullify references when no longer needed to allow garbage collection.
Root cause:Not realizing closures keep references alive can cause unexpected memory retention.
Key Takeaways
Lambdas with captures, or closures, are functions that remember variables from where they were created, allowing flexible and powerful code.
Captured variables are held by reference, not copied, so changes to those variables affect the lambda's behavior.
Closures extend the lifetime of captured variables beyond their original scope by storing them in hidden compiler-generated classes.
Common pitfalls include capturing loop variables incorrectly and unintentionally keeping large objects alive, which can cause bugs and memory issues.
Understanding closures deeply helps write better asynchronous code, event handlers, and functional patterns while avoiding subtle bugs.