开发者

How does a lambda in C# bind to the enumerator in a foreach?

I just came across the most unexpected behavior. I'm sure there is a good reason it works this way. Can someone help explain this?

Consider this code:

var nums = new int[] { 1, 2, 3, 4 };
var actions = new List<Func<int>>();

foreach (var num in nums)
{
    actions.Add(() => num);
}

foreach (var num in nums)
{
    var x = num;
    actions.Add(() => x);
}

foreach (var action in actions)
{
    Debug.Write(action() + " ");
}

The output is a bit surprising for me:

4 4 4 4 1 2 3 4 

Obviously there's something going on with how the lambda is refer开发者_Go百科encing the enumerator. In the first version of the foreach, is 'num' actually bound to 'Current', instead of the result returned by it?


This is well-known and established behavior regarding lambdas, though frequently surprising to those who've encountered it for the first time. The fundamental issue is that your mental model of what a lambda is isn't quite correct.

A lambda is a function that doesn't get run until it's invoked. Your closure binds a reference to that lambda instance, not the value. When you execute your actions in your final foreach loop, that's the first time you're actually following the closed reference to see what it is.

In the first case, you're referencing num, and at that point, the value of num is 4, so of course all your output is 4. In the second case, each lambda has been bound to a different value that was local to the loop each time, and that value isn't changed (it hasn't been GC'd solely because of the lambda reference.) therefore, you get the answer that you expect.

The closure over a local temporary value is actually the standard approach to capture a specific value from a point in time within the lambda.

Adam's link to Eric Lippert's blog provides a more in-depth (and technically accurate) description of what's going on.


See Eric Lippert's blog post on this issue; it has to do with how iterator variables are scoped in code, and how that applies to lambda closures and hoisted functions.


Since the foreach construct is just syntactic sugar it is best to think of it in it's true form.

int num;
while (nums.MoveNext())
{
    num = nums.Current;
    actions.Add(() => num);
}

The lambda will capture the num variable so when you execute the lambda the latest value of num will be used.


This is because of two following things:
1) delegates save context (scope) of outside variables
2) first foreach cycle will compile in only one "num" variable declared.
3) lazy evaluation

Each delegated added in the first cycle will save the same num variable saved to the scope. Because of lazy evaluation you will run the delegates after the first cycle is finished, so num veraible, saved to the delegates' scope equals 4.


See On lambdas, capture, and mutability

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜