55

I was just writing some quick code and noticed this complier error

Using the iteration variable in a lambda expression may have unexpected results.
Instead, create a local variable within the loop and assign it the value of the iteration variable.

I know what it means and I can easily fix it, not a big deal.
But I was wondering why it is a bad idea to use a iteration variable in a lambda?
What problems can I cause later on?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Nathan W
  • 54,475
  • 27
  • 99
  • 146
  • related: http://stackoverflow.com/questions/190227/building-a-linq-query-programatically-without-local-variables-tricking-me – nawfal Nov 02 '13 at 07:09
  • better if you give an example where it actually works / gives the right result! eg look at the result here http://pastebin.com/raw/FghmXkby it's not right.. consistently the same wrong result. – barlop Mar 13 '17 at 13:37
  • An implementation so surprisingly intuitive that there's 500,000 questions and 9,000 blog posts about it... what is this, C++? – jrh Mar 21 '19 at 17:24

3 Answers3

56

Consider this code:

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

for (int i = 0; i < 10; i++)
{
    actions.Add(() => Console.WriteLine(i));
}

foreach (Action action in actions)
{
    action();
}

What would you expect this to print? The obvious answer is 0...9 - but actually it prints 10, ten times. It's because there's just one variable which is captured by all the delegates. It's this kind of behaviour which is unexpected.

EDIT: I've just seen that you're talking about VB.NET rather than C#. I believe VB.NET has even more complicated rules, due to the way variables maintain their values across iterations. This post by Jared Parsons gives some information about the kind of difficulties involved - although it's back from 2007, so the actual behaviour may have changed since then.

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
Jon Skeet
  • 1,421,763
  • 867
  • 9,128
  • 9,194
  • 6
    In two words: lambda are not necessarly evaluated while looping, and when they are called the iteration variable may be out of scope, unallocated, or with it's final value (even beyond the loop limit). – BertuPG Feb 08 '11 at 15:03
  • 10
    @BertuPG: Which of those were the two words you were thinking of? ;) – Jon Skeet Feb 08 '11 at 15:05
  • @Joh: oh... yeah... so let me replace "words" with "phrases" ^^ – BertuPG Feb 08 '11 at 16:59
  • 4
    I smell an interview question. :-) – EightyOne Unite Jul 07 '11 at 10:24
  • I know this is an old answer but anyway.... Paul Vick reset his blog & deleted that link. [This post](http://blogs.msdn.com/b/vbteam/archive/2007/07/26/closures-in-vb-part-5-looping.aspx) by Jared might be a good replacement – MarkJ Feb 01 '12 at 21:03
  • 2
    I've noticed that VB displays the warning mentioned in the question but C# doesn't (using VS2015 and .NET 4.5.2) although the behavior is the same (10, ten times). Not sure if this has always been the case? – 41686d6564 stands w. Palestine Jun 22 '19 at 17:35
  • @AhmedAbdelhameed: I don't know, I'm afraid. – Jon Skeet Jun 23 '19 at 12:09
8

Assuming you mean C# here.

It's because of the way the compiler implements closures. Using an iteration variable can cause a problem with accessing a modified closure (note that I said 'can' not 'will' cause a problem because sometimes it doesn't happen depending on what else is in the method, and sometimes you actually want to access the modified closure).

More info:

http://blogs.msdn.com/abhinaba/archive/2005/10/18/482180.aspx

Even more info:

http://blogs.msdn.com/oldnewthing/archive/2006/08/02/686456.aspx

http://blogs.msdn.com/oldnewthing/archive/2006/08/03/687529.aspx

http://blogs.msdn.com/oldnewthing/archive/2006/08/04/688527.aspx

Greg Beech
  • 133,383
  • 43
  • 204
  • 250
  • 1
    It's not "one closure per method" - it's more complicated than that. – Jon Skeet Oct 22 '08 at 22:53
  • 1
    Yeah I realise that read badly - I was trying to paraphrase the situation quickly (Raymond explains in more depth). Removed the offending phrase so people can look at the more info links. – Greg Beech Oct 22 '08 at 22:57
  • 1
    It looks like the links have died but you can still find them here: https://devblogs.microsoft.com/oldnewthing/2006/08/page/4 , "The implementation of anonymous methods and its consequences" (Raymond Chen / Old New Thing Blog) part [1](https://devblogs.microsoft.com/oldnewthing/20060802-00/?p=30263), [2](https://devblogs.microsoft.com/oldnewthing/20060803-00/?p=30253), [3](https://devblogs.microsoft.com/oldnewthing/20060804-00/?p=30233) – jrh Mar 21 '19 at 17:02
4

Theory of Closures in .NET

Local variables: scope vs. lifetime (plus closures) (Archived 2010)

(Emphasis mine)

What happens in this case is we use a closure. A closure is just a special structure that lives outside of the method which contains the local variables that need to be referred to by other methods. When a query refers to a local variable (or parameter), that variable is captured by the closure and all references to the variable are redirected to the closure.

When you are thinking about how closures work in .NET, I recommend keeping these bullet points in mind, this is what the designers had to work with when they were implementing this feature:

  • Note that "variable capture" and lambda expressions are not an IL feature, VB.NET (and C#) had to implement these features using existing tools, in this case, classes and Delegates.
  • Or to put it another way, local variables can't really be persisted beyond their scope. What the language does is make it seem like they can, but it's not a perfect abstraction.
  • Func(Of T) (i.e., Delegate) instances have no way to store parameters passed into them.
  • Though, Func(Of T) do store the instance of the class the method is a part of. This is the avenue the .NET framework used to "remember" parameters passed into lambda expressions.

Well let's take a look!

Sample Code:

So let's say you wrote some code like this:

' Prints 4,4,4,4
Sub VBDotNetSample()
    Dim funcList As New List(Of Func(Of Integer))

    For indexParameter As Integer = 0 To 3
        'The compiler says:
        '   Warning     BC42324 Using the iteration variable in a lambda expression may have unexpected results.  
        '   Instead, create a local variable within the loop and assign it the value of the iteration variable

        funcList.Add(Function()indexParameter)

    Next

    
    For Each lambdaFunc As Func(Of Integer) In funcList
        Console.Write($"{lambdaFunc()}")

    Next

End Sub

You may be expecting the code to print 0,1,2,3, but it actually prints 4,4,4,4, this is because indexParameter has been "captured" in the scope of Sub VBDotNetSample()'s scope, and not in the For loop scope.

Decompiled Sample Code

Personally, I really wanted to see what kind of code the compiler generated for this, so I went ahead and used JetBrains DotPeek. I took the compiler generated code and hand translated it back to VB.NET.

Comments and variable names mine. The code was simplified slightly in ways that don't affect the behavior of the code.

Module Decompiledcode
    ' Prints 4,4,4,4
    Sub CompilerGenerated()

        Dim funcList As New List(Of Func(Of Integer))
        
        '***********************************************************************************************
        ' There's only one instance of the closureHelperClass for the entire Sub
        ' That means that all the iterations of the for loop below are referencing
        ' the same class instance; that means that it can't remember the value of Local_indexParameter
        ' at each iteration, and it only remembers the last one (4).
        '***********************************************************************************************
        Dim closureHelperClass As New ClosureHelperClass_CompilerGenerated

        For closureHelperClass.Local_indexParameter = 0 To 3

            ' NOTE that it refers to the Lambda *instance* method of the ClosureHelperClass_CompilerGenerated class, 
            ' Remember that delegates implicitly carry the instance of the class in their Target 
            ' property, it's not just referring to the Lambda method, it's referring to the Lambda
            ' method on the closureHelperClass instance of the class!
            Dim closureHelperClassMethodFunc As Func(Of Integer) = AddressOf closureHelperClass.Lambda
            funcList.Add(closureHelperClassMethodFunc)
        
        Next
        'closureHelperClass.Local_indexParameter is 4 now.

        'Run each stored lambda expression (on the Delegate's Target, closureHelperClass)
        For Each lambdaFunc As Func(Of Integer) in funcList      
            
            'The return value will always be 4, because it's just returning closureHelperClass.Local_indexParameter.
            Dim retVal_AlwaysFour As Integer = lambdaFunc()

            Console.Write($"{retVal_AlwaysFour}")

        Next

    End Sub

    Friend NotInheritable Class ClosureHelperClass_CompilerGenerated
        ' Yes the compiler really does generate a class with public fields.
        Public Local_indexParameter As Integer

        'The body of your lambda expression goes here, note that this method
        'takes no parameters and uses a field of this class (the stored parameter value) instead.
        Friend Function Lambda() As Integer
            Return Me.Local_indexParameter

        End Function

    End Class

End Module

Note how there is only one instance of closureHelperClass for the entire body of Sub CompilerGenerated, so there is no way that the function could print the intermediate For loop index values of 0,1,2,3 (there's no place to store these values). The code only prints 4, the final index value (after the For loop) four times.

Footnotes:

  • There's an implied "As of .NET 4.6.1" in this post, but in my opinion it's very unlikely that these limitations would change dramatically; if you find a setup where you can't reproduce these results please leave me a comment.

"But jrh why did you post a late answer?"

  • The pages linked in this post are either missing or in shambles.
  • There was no vb.net answer on this vb.net tagged question, as of the time of writing there is a C# (wrong language) answer and a mostly link only answer (with 3 dead links).
jrh
  • 405
  • 2
  • 10
  • 29
  • Just FYI, if anyone else is playing around with the code and you get hard crashes to desktop when you rename `closureHelperClass`, looks like it's [due to a bug in Visual Studio](https://github.com/dotnet/roslyn/issues/21662), save often when using rename / refactor! – jrh Apr 10 '19 at 13:51