Skip to main content
ANVISoftware Solutions
Lesson 21 of 62Beginner14 min

Scope and Lifetime

By the end of this lesson

Predict where a variable is visible and how long it exists.

Scope is where a name can be used. Lifetime is how long the value behind it exists. Most of the time they match, and the interesting cases are the ones where they do not.

Both are decided by braces. A pair of braces makes a block, and a variable declared inside it belongs to it.

Blocks decide what is visible
C#
decimal subtotal = 100m;

if (subtotal > 50m)
{
    decimal discount = subtotal * 0.05m;   // exists only inside these braces
    subtotal -= discount;
}

// Console.WriteLine(discount);   // build error: not in scope out here

for (int i = 0; i < 3; i++)
{
    Console.WriteLine($"Pass {i}");
}

// Console.WriteLine(i);          // build error: i belonged to the loop

Console.WriteLine(subtotal);      // 95 — declared outside, so still available
  • A variable exists from its declaration to the closing brace of its block, and nowhere else.
  • A for loop's counter belongs to the loop. Needing it afterwards means declaring it before the loop starts.
  • An inner block can see the variables of the block around it. The reverse is never true, which is why discount cannot be read below.
  • C# also refuses to let an inner block declare a name that is already in scope. Other languages allow that shadowing, and it produces bugs where a reader believes they are looking at the outer variable.

The four places a variable can live:

Local
Declared inside a method or block. Visible from its declaration to the end of that block, and gone afterwards.
Parameter
Visible throughout the method body. It is an ordinary variable, so it can be assigned to — which changes the copy, not the caller's value.
Field
Declared in a class. Visible to every member of that class, and it lives as long as the object does.
Static field
One copy shared by the whole type, alive for as long as the program runs. Convenient, and the easiest way to create a bug that only appears when two things happen at once.
When a local and a field share a name
C#
var item = new StockItem(10);

item.Receive(5);
Console.WriteLine(item.Quantity);   // 10 — the delivery was lost

public class StockItem
{
    private int quantity;

    public StockItem(int quantity)
    {
        this.quantity = quantity;   // this. means the field; the bare name means the parameter
    }

    public int Quantity => quantity;

    public void Receive(int delivered)
    {
        int quantity = this.quantity + delivered;   // a new local that hides the field
        Console.WriteLine($"Now holding {quantity}");
    }
}
  • In the constructor, the parameter and the field have the same name, so the plain name refers to the parameter. this.quantity says you mean the field.
  • In Receive, the local hides the field for the rest of the method. The code compiles, prints a believable number, and the field is never updated.
  • Nothing here is an error, which is what makes it worth knowing. The compiler cannot tell that you meant the field.
  • Two habits avoid it: name locals differently from fields, and keep the constructor as the only place where the two names meet.

Lifetime, which is a separate question from visibility:

  • A local's value normally disappears when its block ends
  • A field lives as long as its object; the object lives as long as something can still reach it
  • The garbage collector reclaims unreachable objects at a time of its choosing, not the moment the last variable goes out of scope
  • A static field lives for the whole run of the program, which makes it a common reason memory is never released
  • A local captured by a lambda outlives its block, because the lambda still needs it
A captured loop variable behaves differently in for and foreach
C#
List<Action> fromFor = new List<Action>();

for (int i = 0; i < 3; i++)
{
    fromFor.Add(() => Console.Write(i));   // all three capture the same i
}

foreach (Action print in fromFor)
{
    print();      // 333
}

Console.WriteLine();

List<Action> fromForeach = new List<Action>();

foreach (int value in new[] { 0, 1, 2 })
{
    fromForeach.Add(() => Console.Write(value));   // a fresh value each pass
}

foreach (Action print in fromForeach)
{
    print();      // 012
}

Console.WriteLine();
  • A for loop has one counter for all its passes. All three lambdas captured that one variable, and by the time they run it holds 3.
  • A foreach declares a new variable on each pass, so each lambda captured a different one. C# changed this in version 5 precisely because the old shared behaviour surprised everybody.
  • To fix the for version, copy the counter into a variable declared inside the body and capture that instead.
  • This is also proof that a captured local outlives its block: the lambdas ran after the loop finished, and the values were still there.

Summary

  • A variable is visible from its declaration to the closing brace of its block, and inner blocks can see outer ones
  • C# forbids shadowing a name already in scope, but a local may still hide a field
  • Lifetime is not visibility: an object survives while anything can reach it, and is collected later
  • A local captured by a lambda outlives its block, which is why a shared for counter gives the wrong values
  • Static fields live for the whole program and are shared by everything, including concurrent work

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Try it yourself

Write a small class with a private field and a method that declares a local of the same name, then print the field afterwards to confirm it never changed.

Fix it twice: once with this., and once by renaming. Decide which fix you would keep.

Show solution

this.quantity += delivered; is the smallest fix and it works. It relies on every future reader noticing one four-character prefix, on a line that otherwise looks fine.

Renaming the local removes the question altogether. The compiler no longer has two candidates, and a reviewer has nothing to check.

Most teams keep the rename and accept this. only where it is genuinely required, which is a constructor parameter matching its field. The principle is that correctness which depends on the reader spotting something is fragile.

C#
var item = new StockItem(10);

item.Receive(5);
Console.WriteLine(item.Quantity);   // 15

public class StockItem
{
    private int quantity;

    public StockItem(int quantity)
    {
        this.quantity = quantity;
    }

    public int Quantity => quantity;

    public void Receive(int delivered)
    {
        int newQuantity = quantity + delivered;   // no name clash to misread
        quantity = newQuantity;
    }
}

Challenge

Challenge

A for loop builds a list of three actions, each meant to print its own pass number. All three print 3.

Fix it without changing the loop to a foreach, then explain what your fix changed about the variable's lifetime.

Show solution

Copying the counter into a variable declared inside the loop body gives each pass its own variable. Each lambda then captures a different one, and each of those survives as long as its lambda does.

The counter itself is untouched by the fix. It is still one variable shared across all three passes, and it still ends at 3 — the lambdas are no longer looking at it.

This is the same reason foreach was changed in C# 5: a per-pass variable is almost always what the author meant. Knowing why the fix works matters more than the fix, because the same capture rule applies to any lambda that closes over a changing local.

C#
List<Action> actions = new List<Action>();

for (int i = 0; i < 3; i++)
{
    int pass = i;                                  // a new variable on every pass
    actions.Add(() => Console.Write(pass));
}

foreach (Action print in actions)
{
    print();      // 012
}

Console.WriteLine();

Saved in this browser only.