C# Lambdas: Do You Know What You Captured?

The Sun is blue?!
If nothing else helps, read the manual
Captured variables in loops
Captured iteration variables
Conclusion

The Sun Is Blue?

This article as many others on this site, has started with a bug. The behavior of captured variables in lambdas may be, well... surprising. I found it the hard way. I was so astonished, I decided to write an article about it. The code that demonstrates the issue looks like this:

var colors = new[] { "red", "green", "blue" };
var actions = new List<Action<string>>();
 
foreach (var color in colors)
{
    actions.Add(s => Console.WriteLine(s + " is " + color));
}
 
actions[0]("Sun");
actions[1]("Grass");
actions[2]("Sky");

I would think that the first action would add the words "is red" to the argument, the second would add "is green", and the third would add "is blue". In reality they all add "is blue", and the program prints

Sun is blue
Grass is blue
Sky is blue
Press any key to continue . . .

Either something strange is going on, or we forgot to take off our blue glasses. It turns out, this is not a bug, this is a feature. The program behaves correctly according to the C# language specification. It all has to do with the way lambda expressions capture external variables, such as color in our example.

If Nothing Else Helps, Read The Manual

C# specification version 3.0 specifies behavior of captured variables in paragraph 7.14.4. The specification comes with Visual Studio 2008, and can be found at C:\Program Files\Microsoft Visual Studio 9.0\VC#\Specifications\1033\CSharp Language Specification.doc or similar location.

In functional languages all or most objects are immutable, so outer variables captured in Lambda experssions typically remain constant. It is different in C#: outer variables are captured "by reference". Whenever a value of the outer variable chagnes, it affects all lambda expressions that captured it.

When an outer variable is referenced by an anonymous function, the outer variable is said to have been captured by the anonymous function... The lifetime of a captured outer variable is extended at least until the delegate or expression tree created from the anonymous function becomes eligible for garbage collection.

It is even possible to create "entangled" lambdas that communicate through a captured variable:

private void GetEntangledLambdas(out Func<int> getter, out Action<int> setter)
{
    int x = 0;
    getter = () => x;
    setter = v => {x = v;};
}
 
[TestMethod]
public void Lambads_Can_Communicate_Through_Captured_Variable()
{
    Func<int> getter;
    Action<int> setter;
    GetEntangledLambdas(out getter, out setter);
    setter(10);
    Assert.AreEqual(10, getter());
    setter(20);
    Assert.AreEqual(20, getter());
}

Captured Variables in Loops

For variables in loops it becomes important where the variable is declared. Variables declared inside a loop will be instantiated multiple times (paragraph 7.14.4.2). In the example below each lambda references its own copy of x, and thus they return different values:

List<Func<int>> GetListOfLambdas()
{
    var result = new List<Func<int>>();
 
    for (int i = 0; i < 3; ++i)
    {
        int x = i;
        result.Add(() => x);
    }
    return result;
}
 
[TestMethod]
public void Variable_Declared_In_A_Loop_Instantiated_Multiple_Times()
{
    var list = GetListOfLambdas();
    Assert.AreEqual(0, list[0]());
    Assert.AreEqual(1, list[1]());
    Assert.AreEqual(2, list[2]());
}

However, if x is declared outside the loop, all lambdas will share the same copy of x and return the same value, which is the latest value ox x:

List<Func<int>> GetListOfLambdas2()
{
    var result = new List<Func<int>>();
    int x;
 
    for (int i = 0; i < 3; ++i)
    {
        x = i;
        result.Add(() => x);
    }
    return result;
}
 
[TestMethod]
public void Variable_Outside_The_Loop_Instantiated_Once()
{
    var list = GetListOfLambdas2();
    Assert.AreEqual(2, list[0]());
    Assert.AreEqual(2, list[1]());
    Assert.AreEqual(2, list[2]());
}

Captured Iteration Variables

And now, the most surprising part of all:

If a for-loop declares an iteration variable, that variable itself is considered to be declared outside of the loop

This rule applies only to the instantiation in lambdas. For all other purposes the iteration variable works as if it were declared inside the loop. E.g., it is possible to have two loops with the same iteration variable name in the same scope:

for (int i = 0; i < 10; ++i) ;
for (int i = 0; i < 10; ++i) ; // legal

If i were considered declared outside the loop, we would end up with two conflicting declaration for i. What happens if we combine the above examples? Let's generate lambdas in two consequtive loops that have the same iteration variable name:

List<Func<int>> GetListOfLambdas3()
{
    var result = new List<Func<int>>();
 
    for (int i = 0; i < 3; ++i)
    {
        result.Add(() => i);
    }
 
    for (int i = 3; i < 6; ++i)
    {
        result.Add(() => i);
    }
 
    return result;
}
 
[TestMethod]
public void Loops_Dont_Share_Iteration_Variables()
{
    var list = GetListOfLambdas3();
    Assert.AreEqual(3, list[0]());
    Assert.AreEqual(3, list[1]());
    Assert.AreEqual(3, list[2]());
    Assert.AreEqual(6, list[3]());
    Assert.AreEqual(6, list[4]());
    Assert.AreEqual(6, list[5]());
}

What we learn from this example is that:

  • Captured iteration variables are not shared between loops, even if they have the same name (like i). There is one instance of the iteration variable per loop, and this instance is shared by all lambdas created in that loop.
  • In a loop like for (int i=0; i<n; ++i) the iteration variable retains a value of n upon exit.

Conclusion

Capturing of outer variables in C# lambdas may be counterintuitive, especially when loops are involved. Lambdas are a product of functional programming, and we expect them to behave accordingly and operate on immutable objects. In C# lambdas are just functinos that operate on mutable captured variables. Several lambdas may share the same instance of a captured variable, and even communication via side effects. Furthermore, all lambdas in a for or foreach loop would share the same instance of the loop iteration variable, which may lead to subtle bug. As usual, documentation is your friend. Even if C# behavior may be surprising, it is at least well documented.