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.
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:
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 ofn
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.
Copyright (c) Ivan Krivyakov. Last updated: May 23, 2010