爲什麼ReSharper告訴我「隱式捕獲關閉」?

2020-08-12 10:39:39

本文翻譯自:Why does ReSharper tell me 「implicitly captured closure」?

I have the following code: 我有以下程式碼:

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;
}

Now, I've added a comment on the line that ReSharper is suggesting a change. 現在,我在ReSharper建議改變的行上新增了評論。 What does it mean, or why would it need to be changed? 這是什麼意思,或者爲什麼需要改變? implicitly captured closure: end, start


#1樓

參考:https://stackoom.com/question/VcJ3/爲什麼ReSharper告訴我-隱式捕獲關閉


#2樓

The warning tells you that the variables end and start stay alive as any of the lambdas inside this method stay alive. 該警告告訴您變數endstart保持活動狀態,因爲此方法中的任何lambda都保持活動狀態。

Take a look at the short example 看看簡短的例子

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();
}

I get an "Implicitly captured closure: g" warning at the first lambda. 我在第一個lambda上得到了一個「隱式捕獲的閉包:g」警告。 It is telling me that g cannot be garbage collected as long as the first lambda is in use. 它告訴我,只要第一個lambda正在使用中, g就不能被垃圾收集

The compiler generates a class for both lambda expressions and puts all variables in that class which are used in the lambda expressions. 編譯器爲兩個lambda表達式生成一個類,並將所有變數放在lambda表達式中使用的該類中。

So in my example g and i are held in the same class for execution of my delegates. 所以在我的例子中, gi被儲存在同一個類中以執行我的代理。 If g is a heavy object with a lot of resources left behind, the garbage collector couldn't reclaim it, because the reference in this class is still alive as long as any of the lambda expressions is in use. 如果g是一個有大量資源的重型物件,垃圾收集器就無法回收它,因爲只要正在使用任何lambda表達式,該類中的參照仍然存在。 So this is a potential memory leak, and that is the reason for the R# warning. 所以這是潛在的記憶體漏失,這就是R#警告的原因。

@splintor As in C# the anonymous methods are always stored in one class per method there are two ways to avoid this: @splintor與在C#中一樣,匿名方法總是儲存在每個方法的一個類中,有兩種方法可以避免這種情況:

  1. Use an instance method instead of an anonymous one. 使用實體方法而不是匿名方法。

  2. Split the creation of the lambda expressions into two methods. 將lambda表達式的建立拆分爲兩個方法。


#3樓

Agreed with Peter Mortensen. 同意Peter Mortensen。

The C# compiler generates only one type that encapsulates all variables for all lambda expressions in a method. C#編譯器只生成一個型別,它封裝了方法中所有lambda表達式的所有變數。

For example, given the source code: 例如,給定原始碼:

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
    }
}

The compiler generates a type looks like : 編譯器生成的型別如下:

[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();
  }
}

And the Capture method is compiled as: 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));
}

Though the second lambda does not use x , it cannot be garbage collected as x is compiled as a property of the generated class used in the lambda. 雖然第二個lambda不使用x ,但它不能被垃圾收集,因爲x被編譯爲lambda中使用的生成類的屬性。


#4樓

For Linq to Sql queries, you may get this warning. 對於Linq to Sql查詢,您可能會收到此警告。 The lambda's scope may outlive the method due to the fact that the query is often actualized after the method is out of scope. 由於查詢通常在方法超出範圍後實現,因此lambda的範圍可能比方法更長。 Depending on your situation, you may want to actualize the results (ie via .ToList()) within the method to allow for GC on the method's instance vars captured in the L2S lambda. 根據您的具體情況,您可能希望在方法中實現結果(即通過.ToList()),以允許在L2S lambda中捕獲的方法範例變數上使用GC。


#5樓

The warning is valid and displayed in methods that have more than one lambda , and they capture different values . 警告有效並顯示在具有多個lambda的方法中 ,並且它們捕獲不同的值

When a method that contains lambdas is invoked, a compiler-generated object is instantiated with: 當呼叫包含lambdas的方法時,將使用以下程式碼範例化編譯器生成的物件:

  • instance methods representing the lambdas 表示lambda的實體方法
  • fields representing all values captured by any of those lambdas 表示由任何 lambda捕獲的所有值的欄位

As an example: 舉個例子:

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++; });
    }
}

Examine the generated code for this class (tidied up a little): 檢查這個類的生成程式碼(整理一下):

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; }
    }
}

Note the instance of LambdaHelper created stores both p1 and p2 . 請注意, LambdaHelper建立的範例儲存了p1p2

Imagine that: 設想:

  • callable1 keeps a long-lived reference to its argument, helper.Lambda1 callable1保持對其參數helper.Lambda1的長期參照
  • callable2 does not keep a reference to its argument, helper.Lambda2 callable2不會保持對其參數helper.Lambda2

In this situation, the reference to helper.Lambda1 also indirectly references the string in p2 , and this means that the garbage collector will not be able to deallocate it. 在這種情況下,對helper.Lambda1的參照也間接參照了p2的字串,這意味着垃圾收集器將無法解除分配它。 At worst it is a memory/resource leak. 在最壞的情況下,它是記憶體/資源泄漏。 Alternatively it may keep object(s) alive longer than otherwise needed, which can have an impact on GC if they get promoted from gen0 to gen1. 或者,它可以使物件保持比其他需要更長的時間,如果它們從gen0升級到gen1,則可能對GC產生影響。


#6樓

You could always figure out with a reasons of R# suggestions just by clicking on the hints like shown below: 你總是可以通過單擊如下所示的提示找出R#建議的原因:

在此输入图像描述

This hint will direct you here . 這個提示會引導你到這裏來


This inspection draws your attention to the fact that more closure values are being captured than is obviously visibly, which has an impact on the lifetime of these values. 這種檢查使您注意到這樣一個事實,即捕獲的閉合值明顯高於明顯可見的閉合值,這會對這些值的壽命產生影響。

Consider the following code: 請考慮以下程式碼:

using System; 使用系統; public class Class1 { private Action _someAction; 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); }; } } 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 

the second closure, we can see that obj1 is being explicitly captured, but ReSharper is warning us that obj2 is being implicitly captured. 第二個閉包,我們可以看到obj1被明確捕獲,但是ReSharper警告我們obj2被隱式捕獲。

This is due to an implementation detail in the C# compiler. 這是由於C#編譯器中的實現細節。 During compilation, closures are rewritten into classes with fields that hold the captured values, and methods that represent the closure itself. 在編譯期間,閉包被重寫爲具有儲存捕獲值的欄位的類,以及表示閉包本身的方法。 The C# compiler will only create one such private class per method, and if more than one closure is defined in a method, then this class will contain multiple methods, one for each closure, and it will also include all captured values from all closures. C#編譯器每個方法只會建立一個這樣的私有類,如果在方法中定義了多個閉包,那麼這個類將包含多個方法,每個閉包一個,並且它還包括所有閉包的所有捕獲值。

If we look at the code that the compiler generates, it looks a little like this (some names have been cleaned up to ease reading): 如果我們檢視編譯器生成的程式碼,它看起來有點像這樣(有些名稱已被清理以方便閱讀):

public class Class1 { [CompilerGenerated] private sealed class <>c__DisplayClass1_0 { public object obj1; public class Class1 {[CompilerGenerated] private sealed class <> c__DisplayClass1_0 {public object obj1; public object obj2; 公共物件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 

in one of the closures, it will still be captured. 在其中一個閉包中,它仍將被捕獲。 This is the "implicit" capture that ReSharper is highlighting. 這是ReSharper強調的「隱式」捕獲。

The implication of this inspection is that the implicitly captured closure value will not be garbage collected until the closure itself is garbage collected. 這種檢查的含義是,在關閉本身被垃圾收集之前,隱式捕獲的閉包值不會被垃圾收集。 The lifetime of this value is now tied to the lifetime of a closure that does not explicitly use the value. 此值的生命週期現在與未明確使用該值的閉包的生命週期相關聯。 If the closure is long lived, this might have a negative effect on your code, especially if the captured value is very large. 如果閉包是長壽的,這可能會對您的程式碼產生負面影響,特別是如果捕獲的值非常大。

Note that while this is an implementation detail of the compiler, it is consistent across versions and implementations such as Microsoft (pre and post Roslyn) or Mono's compiler. 請注意,雖然這是編譯器的實現細節,但它在Microsoft(Roslyn之前和之後)或Mono編譯器等版本和實現中是一致的。 The implementation must work as described in order to correctly handle multiple closures capturing a value type. 實現必須按照描述的方式工作,以便正確處理捕獲值型別的多個閉包。 For example, if multiple closures capture an int, then they must capture the same instance, which can only happen with a single shared private nested class. 例如,如果多個閉包捕獲一個int,那麼它們必須捕獲相同的範例,這隻能在一個共用的私有巢狀類中發生。 The side effect of this is that the lifetime of all captured values is now the maximum lifetime of any closure that captures any of the values. 這樣做的副作用是所有捕獲值的生命週期現在是捕獲任何值的任何閉包的最大生命週期。