web-dev-qa-db-ja.com

ラムダ式で反復変数を使用するのが悪いのはなぜですか

簡単なコードを書いていたところ、このコンパイラエラーに気づきました

ラムダ式で反復変数を使用すると、予期しない結果が生じる可能性があります。
代わりに、ループ内にローカル変数を作成し、それに反復変数の値を割り当てます。

私はそれが何を意味するのかを知っており、大したことではなく、簡単に修正できます。
しかし、ラムダで反復変数を使用するのはなぜ悪い考えなのか疑問に思いました。
後でどのような問題を引き起こす可能性がありますか?

51
Nathan W

このコードを検討してください:

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

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

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

これが何を印刷すると思いますか?明白な答えは0 ... 9ですが、実際には10、10回印刷されます。これは、すべてのデリゲートによってキャプチャされる変数が1つしかないためです。意外なのはこのような振る舞いです。

編集:あなたがC#ではなくVB.NETについて話しているのを見たばかりです。 VB.NETには、変数が反復間で値を維持する方法のために、さらに複雑なルールがあると思います。 Jared Parsonsによるこの投稿 関連する問題の種類に関する情報を提供します-2007年から戻っているため、実際の動作はそれ以降変更されている可能性があります。

51
Jon Skeet

ここでC#を意味すると仮定します。

これは、コンパイラがクロージャを実装する方法が原因です。反復変数canを使用すると、変更されたクロージャへのアクセスで問題が発生します(「できない」と言ったので、問題が発生することがあります。メソッド内の他の内容によっては発生しません。実際に変更されたクロージャにアクセスしたい場合もあります)。

より詳しい情報:

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

さらに詳しい情報:

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

7
Greg Beech

.NETにおけるクロージャの理論

ローカル変数:スコープとライフタイム(およびクロージャ) (2010年にアーカイブ)

(エンファシスマイン)

この場合に起こることは、クロージャを使用することです。クロージャは、他のメソッドで参照する必要のあるローカル変数を含むメソッドの外部に存在する特別な構造です。 クエリがローカル変数(またはパラメーター)を参照する場合、その変数はクロージャーによってキャプチャされ、変数へのすべての参照はクロージャーにリダイレクトされます。

.NETでクロージャがどのように機能するかを考えるときは、次の箇条書きを覚えておくことをお勧めします。これは、設計者がこの機能を実装するときに使用しなければならなかったことです。

  • 「変数キャプチャ」とラムダ式はIL機能ではないことに注意してください。VB.NET(およびC#)は、既存のツール(この場合はクラスとDelegates)を使用してこれらの機能を実装する必要がありました。
  • 言い換えれば、ローカル変数は実際にはその範囲を超えて永続化することはできません。言語が行うことは、できるように見えるにすることですが、完全な抽象化ではありません。
  • Func(Of T)(つまり、Delegate)インスタンスには、渡されたパラメーターを格納する方法がありません。
  • ただし、Func(Of T)は、メソッドが含まれるクラスのインスタンスを格納します。これは、ラムダ式に渡されたパラメーターを「記憶」するために使用される.NETフレームワークの手段です。

さて見てみましょう!

サンプルコード:

したがって、次のようなコードを記述したとしましょう。

' 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

コードが0,1,2,3を出力することを期待しているかもしれませんが、実際には4,4,4,4を出力します。これは、indexParameterSub VBDotNetSample()のスコープであり、Forループスコープにはありません。

逆コンパイルされたサンプルコード

個人的には、コンパイラがこのためにどのようなコードを生成するのかを本当に知りたかったので、先に進んでJetBrainsDotPeekを使用しました。コンパイラで生成されたコードを取得し、VB.NETに手動で変換し直しました。

コメントと変数名は私のものです。コードは、コードの動作に影響を与えない方法でわずかに簡略化されました。

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

Sub CompilerGeneratedの本体全体に対してclosureHelperClassのインスタンスが1つしかないため、関数が中間のForループインデックス値0,1を出力する方法がないことに注意してください。 、2,3(これらの値を格納する場所はありません)。このコードは、最終的なインデックス値(Forループの後)を4回だけ出力します。

脚注:

  • この投稿には暗黙の「As.NET4.6.1」がありますが、私の意見では、これらの制限が劇的に変わる可能性はほとんどありません。これらの結果を再現できない設定を見つけた場合は、コメントを残してください。

「しかし、なぜあなたは遅い答えを投稿したのですか?」

  • この投稿にリンクされているページが欠落しているか、混乱しています。
  • このvb.netタグ付きの質問にはvb.netの回答はありませんでした。これを書いている時点では、C#(間違った言語)の回答とほとんどリンクのみの回答(3つのリンク切れ)があります。
0
jrh