web-dev-qa-db-ja.com

ReSharperが「暗黙的にキャプチャされたクロージャ」と表示するのはなぜですか?

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

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

285
PiousVenom

この警告は、変数endおよびstartが、このメソッド内のラムダのいずれかが生存しているため、生存していることを示しています。

短い例を見てみましょう

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」という警告が表示されます。最初のラムダが使用されている限り、ggarbage collected にすることはできません。

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

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

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

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

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

383
Console

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

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
Smartkid

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

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

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

例として:

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に影響を与える可能性があります。

28
Drew Noakes

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

3
Jason Dufair

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

enter image description here

このヒントは here を指示します。


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

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

システムを使用して;パブリッククラス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);
    };
} } In the first closure, we see that both obj1 and obj2 are being explicitly captured; we can see this just by looking at the code. For

2番目のクロージャーでは、obj1が明示的にキャプチャされていることがわかりますが、ReSharperはobj2が暗黙的にキャプチャされていることを警告しています。

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

コンパイラが生成するコードを見ると、次のように見えます(読みやすいように一部の名前が整理されています)。

パブリッククラスClass1 {[CompilerGenerated]プライベートシールクラス<> c__DisplayClass1_0 {パブリックオブジェクトobj1;パブリックオブジェクト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);
} } When the method runs, it creates the display class, which captures all values, for all closures. So even if a value isn't used

クロージャの1つでは、まだキャプチャされます。これは、ReSharperが強調している「暗黙の」キャプチャです。

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

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

2
anatol