ReSharperが「暗黙的にキャプチャされたクロージャ」を教えてくれるのはなぜですか?


296

私は次のコードを持っています:

public double CalculateDailyProjectPullForceMax(DateTime date, string start = null, string end = null)
{
    Log("Calculating Daily Pull Force Max...");

    var pullForceList = start == null
                             ? _pullForce.Where((t, i) => _date[i] == date).ToList() // implicitly captured closure: end, start
                             : _pullForce.Where(
                                 (t, i) => _date[i] == date && DateTime.Compare(_time[i], DateTime.Parse(start)) > 0 && 
                                           DateTime.Compare(_time[i], DateTime.Parse(end)) < 0).ToList();

    _pullForceDailyMax = Math.Round(pullForceList.Max(), 2, MidpointRounding.AwayFromZero);

    return _pullForceDailyMax;
}

ここで、ReSharperが変更を提案しているという行にコメントを追加しました。それはどういう意味ですか、なぜ変更する必要があるのですか?implicitly captured closure: end, start


6
MyCodeSucksは受け入れられた回答を修正してください:kevingessnerの回答は間違っており(コメントで説明されています)、受け入れられているとマークされていると、ユーザーがコンソールの回答に気付かないと誤解を招きます。
Albireo 2014年

1
また、try / catchの外でリストを定義し、すべての追加をtry / catchで行って、結果を別のオブジェクトに設定した場合にも、この問題が発生することがあります。try / catch内で定義/追加を移動すると、GCが許可されます。うまくいけば、これは理にかなっています。
Micah Montoya

回答:


391

警告は、このメソッド内の任意のラムダが生き続けるので、変数endstart生き続けることを通知します。

短い例を見てください

protected override void OnLoad(EventArgs e)
{
    base.OnLoad(e);

    int i = 0;
    Random g = new Random();
    this.button1.Click += (sender, args) => this.label1.Text = i++.ToString();
    this.button2.Click += (sender, args) => this.label1.Text = (g.Next() + i).ToString();
}

最初のラムダで「暗黙的にキャプチャされたクロージャ:g」警告が表示されます。最初のラムダが使用れている限り、ガベージコレクションを実行gできないと言われています。

コンパイラは両方のラムダ式のクラスを生成し、ラムダ式で使用されるすべての変数をそのクラスに配置します。

だから私の例giは、私のデリゲートの実行のために同じクラスで開催されます。gが多くのリソースが残された重いオブジェクトである場合、ラムダ式のいずれかが使用されている限り、このクラスの参照はまだ有効であるため、ガベージコレクターはそれを再利用できませんでした。したがって、これは潜在的なメモリリークであり、それがR#警告の理由です。

@splintor C#と同様に、匿名メソッドは常にメソッドごとに1つのクラスに格納されます。これを回避するには2つの方法があります。

  1. 匿名メソッドの代わりにインスタンスメソッドを使用します。

  2. ラムダ式の作成を2つのメソッドに分割します。


30
このキャプチャを回避するために可能な方法は何ですか?
破片2013

2
このすばらしい回答に感謝します。1つの場所でのみ使用されている場合でも、非匿名メソッドを使用する理由があることを知りました。
ScottRhee 2014年

1
@splintorデリゲート内でオブジェクトをインスタンス化するか、代わりにパラメーターとして渡します。上記のケースでは、私が知る限り、実際にはRandomインスタンスへの参照を保持するのが望ましい動作です。
ケーシー

2
@emodendroket正解です。現時点では、コードのスタイルと読みやすさについて話しています。フィールドの方が推論しやすいです。メモリプレッシャーまたはオブジェクトのライフタイムが重要な場合は、フィールドを選択します。それ以外の場合は、より簡潔なクロージャのままにします。
yzorg 2014

1
私のケースは(かなり)単純化されて、FooとBarを作成するファクトリーメソッドに要約されました。次に、これら2つのオブジェクトによって公開されたイベントにキャプチャランバをサブスクライブします。驚いたことに、FooはBarイベントのランバからのキャプチャを維持し、その逆も同様です。私はC ++の出身ですが、このアプローチは問題なく機能し、ルールがここで異なっていることを知って驚かされました。もっと知っていると思います。
dlf 2016

35

ピーター・モーテンセンに同意しました。

C#コンパイラは、メソッド内のすべてのラムダ式のすべての変数をカプセル化するタイプを1つだけ生成します。

たとえば、次のソースコードがあるとします。

public class ValueStore
{
    public Object GetValue()
    {
        return 1;
    }

    public void SetValue(Object obj)
    {
    }
}

public class ImplicitCaptureClosure
{
    public void Captured()
    {
        var x = new object();

        ValueStore store = new ValueStore();
        Action action = () => store.SetValue(x);
        Func<Object> f = () => store.GetValue();    //Implicitly capture closure: x
    }
}

コンパイラーは次のようなタイプを生成します。

[CompilerGenerated]
private sealed class c__DisplayClass2
{
  public object x;
  public ValueStore store;

  public c__DisplayClass2()
  {
    base.ctor();
  }

  //Represents the first lambda expression: () => store.SetValue(x)
  public void Capturedb__0()
  {
    this.store.SetValue(this.x);
  }

  //Represents the second lambda expression: () => store.GetValue()
  public object Capturedb__1()
  {
    return this.store.GetValue();
  }
}

そして、Captureメソッドは次のようにコンパイルされます:

public void Captured()
{
  ImplicitCaptureClosure.c__DisplayClass2 cDisplayClass2 = new ImplicitCaptureClosure.c__DisplayClass2();
  cDisplayClass2.x = new object();
  cDisplayClass2.store = new ValueStore();
  Action action = new Action((object) cDisplayClass2, __methodptr(Capturedb__0));
  Func<object> func = new Func<object>((object) cDisplayClass2, __methodptr(Capturedb__1));
}

2番目のラムダはを使用しませんが、ラムダで使用される生成クラスのプロパティとしてコンパイルされるため、xガベージコレクションすることはできませんx


31

警告は有効であり、複数のラムダを持つメソッドで表示され、それらは異なる値キャプチャします

ラムダを含むメソッドが呼び出されると、コンパイラによって生成されたオブジェクトが次のようにインスタンス化されます。

  • ラムダを表すインスタンスメソッド
  • それらのラムダのいずれかによってキャプチャされたすべての値を表すフィールド

例として:

class DecompileMe
{
    DecompileMe(Action<Action> callable1, Action<Action> callable2)
    {
        var p1 = 1;
        var p2 = "hello";

        callable1(() => p1++);    // WARNING: Implicitly captured closure: p2

        callable2(() => { p2.ToString(); p1++; });
    }
}

このクラスの生成されたコードを調べます(少し片付けました)。

class DecompileMe
{
    DecompileMe(Action<Action> callable1, Action<Action> callable2)
    {
        var helper = new LambdaHelper();

        helper.p1 = 1;
        helper.p2 = "hello";

        callable1(helper.Lambda1);
        callable2(helper.Lambda2);
    }

    [CompilerGenerated]
    private sealed class LambdaHelper
    {
        public int p1;
        public string p2;

        public void Lambda1() { ++p1; }

        public void Lambda2() { p2.ToString(); ++p1; }
    }
}

とのLambdaHelper両方の作成されたストアのインスタンスに注意してください。p1p2

想像してみろ:

  • callable1 その引数への長期にわたる参照を保持し、 helper.Lambda1
  • callable2 その引数への参照を保持しません、 helper.Lambda2

この状況では、への参照helper.Lambda1も間接的にの文字列を参照します。p2これは、ガベージコレクターが文字列の割り当てを解除できないことを意味します。最悪の場合、メモリ/リソースリークです。または、オブジェクトが必要以上に長く存続する可能性があり、gen0からgen1に昇格した場合、GCに影響を与える可能性があります。


我々はの参照を取り出した場合p1からcallable2、このような:callable2(() => { p2.ToString(); });-これはまだ同じ問題(ガベージコレクタがそれを解放することはできません)が発生しませんLambdaHelper、まだ含まれていますp1とのp2
アントニー

1
はい、同じ問題が存在します。コンパイラはLambdaHelper、親メソッド内のすべてのラムダに対して1つのキャプチャオブジェクト(つまり、上記)を作成します。したがってcallable2、を使用しなくてもp1、と同じキャプチャオブジェクトを共有し、callable1そのキャプチャオブジェクトはとの両方p1を参照しp2ます。これは参照型についてのみ重要でありp1、この例では値型であることに注意してください。
Drew Noakes

3

LinqからSQLへのクエリの場合、この警告が表示されることがあります。メソッドがスコープ外になった後にクエリが実現されることが多いため、ラムダのスコープはメソッドよりも長く存続する可能性があります。状況によっては、メソッド内で(つまり.ToList()を介して)結果を具体化して、L2Sラムダでキャプチャされたメソッドのインスタンス変数のGCを許可することができます。


2

以下に示すようなヒントをクリックするだけで、R#の提案の理由を常に理解できます。

ここに画像の説明を入力してください

このヒントはここであなたを導きます


この検査は、明らかに目に見えるよりも多くのクロージャ値がキャプチャされているという事実に注意を向けます。これは、これらの値の寿命に影響を与えます。

次のコードを検討してください。

using System; 
public class Class1 {
    private Action _someAction;

    public void Method() {
        var obj1 = new object();
        var obj2 = new object();

        _someAction += () => {
            Console.WriteLine(obj1);
            Console.WriteLine(obj2);
        };

        // "Implicitly captured closure: obj2"
        _someAction += () => {
            Console.WriteLine(obj1);
        };
    }
}

最初のクロージャーでは、obj1とobj2の両方が明示的にキャプチャされていることがわかります。コードを見るだけでこれを確認できます。2番目のクロージャでは、obj1が明示的にキャプチャされていることがわかりますが、ReSharperはobj2が暗黙的にキャプチャされていることを警告しています。

これは、C#コンパイラの実装の詳細によるものです。コンパイル中に、クロージャーは、キャプチャーされた値を保持するフィールドと、クロージャー自体を表すメソッドを持つクラスに書き直されます。C#コンパイラは、メソッドごとにこのようなプライベートクラスを1つだけ作成します。メソッドで複数のクロージャが定義されている場合、このクラスには複数のメソッドが含まれます(各クロージャに1つ)。また、すべてのクロージャからキャプチャされたすべての値も含まれます。

コンパイラが生成するコードを見ると、次のようになります(一部の名前は読みやすくするためにクリーンアップされています)。

public class Class1 {
    [CompilerGenerated]
    private sealed class <>c__DisplayClass1_0
    {
        public object obj1;
        public object obj2;

        internal void <Method>b__0()
        {
            Console.WriteLine(obj1);
            Console.WriteLine(obj2);
        }

        internal void <Method>b__1()
        {
            Console.WriteLine(obj1);
        }
    }

    private Action _someAction;

    public void Method()
    {
        // Create the display class - just one class for both closures
        var dc = new Class1.<>c__DisplayClass1_0();

        // Capture the closure values as fields on the display class
        dc.obj1 = new object();
        dc.obj2 = new object();

        // Add the display class methods as closure values
        _someAction += new Action(dc.<Method>b__0);
        _someAction += new Action(dc.<Method>b__1);
    }
}

メソッドが実行されると、すべてのクロージャーについて、すべての値をキャプチャする表示クラスが作成されます。したがって、値がいずれかのクロージャで使用されていない場合でも、キャプチャされます。これは、ReSharperが強調している「暗黙の」キャプチャです。

この検査の意味は、暗黙的にキャプチャされたクロージャ値は、クロージャ自体がガベージコレクションされるまでガベージコレクションされないということです。この値の有効期間は、値を明示的に使用しないクロージャーの有効期間に関連付けられています。クロージャの寿命が長い場合、特にキャプチャした値が非常に大きい場合は、コードに悪影響を与える可能性があります。

これはコンパイラーの実装の詳細ですが、Microsoft(Roslynの前後)やMonoのコンパイラーなどのバージョンと実装全体で一貫しています。実装は、値型をキャプチャする複数のクロージャを正しく処理するために、説明どおりに機能する必要があります。たとえば、複数のクロージャがintをキャプチャする場合、それらは同じインスタンスをキャプチャする必要があります。これは、単一の共有プライベートネストクラスでのみ発生する可能性があります。これの副作用は、すべてのキャプチャされた値の有効期間が、任意の値をキャプチャするすべてのクロージャの最大有効期間になることです。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.