1. Introduction
Remembering what scope is
Let's imagine variables are employees in a big office, and methods, loops, and code blocks are rooms and offices. Some employees are only allowed in their own room, while others can go anywhere in the building. Where each person can be — that's their scope (scope).
Scope defines where a declared variable is “visible” in your program and where you can use it.
Main types of scopes
In C#, you can highlight these main scopes:
| Scope | Example | Where the variable is "visible" |
|---|---|---|
| Local | Inside a method or block | Only inside that block |
| Method parameter | In the method signature | Only inside the method |
| Class variable (field) | In the class body outside methods | In all methods of that class |
| Variable inside a loop/condition | Inside of a loop/if |
Only inside those |
Example with explanations
public class Office
{
int buildingNumber = 50; // Class field: visible in all methods public void PrintInfo() {
int roomNumber = 101; // Local variable: only visible inside PrintInfo if (roomNumber > 100) {
int deskNumber = 5; // Only visible inside this if block Console.WriteLine(deskNumber);
} Console.WriteLine(deskNumber); //
Error! deskNumber isn't visible here anymore
}
}
2. Local functions and scope
Who can "see" whom?
When you declare a local function inside a method (or even inside a loop or condition), it ends up in the same scope as the variables declared above. A local function is kinda like “part of the same room.”
Example
A local function can see variables from the surrounding scope
void PrintWithPrefix(string message)
{
string
prefix = "[LOG]: "; void Print() {
Console.WriteLine(
prefix +
message); // sees both variables!
} Print();
}
Here, the variables prefix and message are visible inside the local function Print because they're declared in the same or a wider scope.
How many scopes are there?
In the example above:
- there's the scope of the method PrintWithPrefix
- inside it — the scope of the function Print
Local functions don't create their own separate “room” completely: they can see everything that's in the hallway (the method).
3. Capturing variables (Capture)
Capturing variables is when a local function uses variables that were declared outside this function, but in the same scope.
Local functions and anonymous methods (lambda expressions, we'll get to those soon) remember (or “capture”) all the variables that were available to them when they were declared.
You can say that functions kinda take a snapshot (capture) of the surrounding world — and can use those variables even when they're called much later.
Diagram
Main method
└─ variable x
└─ local function F() ← "captures" x
Example — the simplest capture
void CounterExample()
{
int counter = 0;
void Increase()
{
counter++; // This function captures the variable counter
}
Increase();
Increase();
Console.WriteLine(counter); // Will print 2
}
Here, after two calls to the local function Increase, the value of counter increases to 2.
4. Using variable capture
Capturing variables lets you easily “pass” data between the method scope and local functions, without messing around with extra parameters.
If there was no capture, you'd have to pass all the variables as parameters:
void CounterExampleWithoutCapture()
{
int counter = 0;
void Increase(ref int c)
{
c++;
}
Increase(ref counter);
Increase(ref counter);
Console.WriteLine(counter);
}
That's annoying — why keep writing ref and mess up the function signature, if it can just “see” the variables outside?
5. Local functions and variable lifetime after the method exits
Do variables live longer than the method?
If you pass a local function (or delegates with lambdas) somewhere outside the current method, the captured variables automatically stop “dying” when the method exits. The CLR (.NET virtual machine) will take care of it — everything you need will be “held by the ears” in memory.
Example: functions live outside the method
Func<int> GetCounter()
{
int count = 0;
int Increment()
{
count++;
return count;
}
return Increment; // Returning the function outside!
}
var counter = GetCounter();
Console.WriteLine(counter()); // 1
Console.WriteLine(counter()); // 2
Here, even after the GetCounter method finishes, the variable count keeps living, because the returned function captured it. This is called a closure — we'll get to that in a separate lecture, but for local functions the mechanism is the same.
6. Typical mistakes and interesting scenarios
Overwriting a variable before calling the local function
Sometimes you might run into a situation where the variable captured by a local function gets changed before you call the function — and then the result might be different from what you expected.
Example:
void Example()
{
int x = 42;
void PrintX() { Console.WriteLine(x); }
x = 100; // Changed the variable!
PrintX(); // Will print 100, not 42!
}
Trick: A local function always sees the latest value of the variable at the moment you call it.
Capturing variables in a for/foreach loop (again)
Classic pain: if you're writing logic in an interview or a big project, always check: am I capturing a “live” variable from a loop, and how will it behave?
GO TO FULL VERSION