Tại sao ReSharper nói với tôi về việc ngầm bắt giữ đóng cửa?


296

Tôi có đoạn mã sau:

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

Bây giờ, tôi đã thêm một nhận xét về dòng mà ReSharper đang đề xuất thay đổi. Điều đó có nghĩa là gì, hoặc tại sao nó cần phải được thay đổi?implicitly captured closure: end, start


6
MyCodeSucks vui lòng sửa câu trả lời được chấp nhận: câu trả lời của kevingessner là sai (như được giải thích trong các bình luận) và việc đánh dấu là chấp nhận sẽ đánh lừa người dùng nếu họ không chú ý câu trả lời của Console.
Albireo

1
Bạn cũng có thể thấy điều này nếu bạn xác định danh sách của mình bên ngoài một lần thử / bắt và thực hiện tất cả việc thêm vào trong lần thử / bắt và sau đó đặt kết quả cho một đối tượng khác. Di chuyển xác định / thêm trong thử / bắt sẽ cho phép GC. Hy vọng điều này có ý nghĩa.
Micah Montoya

Câu trả lời:


391

Cảnh báo cho bạn biết rằng các biến endstarttồn tại như bất kỳ lambdas nào trong phương thức này vẫn tồn tại.

Hãy xem ví dụ ngắn

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

Tôi nhận được cảnh báo "Đóng hoàn toàn bị bắt: g" tại lambda đầu tiên. Nó nói với tôi rằng gkhông thể là rác được thu thập miễn là lambda đầu tiên được sử dụng.

Trình biên dịch tạo một lớp cho cả hai biểu thức lambda và đặt tất cả các biến trong lớp đó được sử dụng trong các biểu thức lambda.

Vì vậy, trong ví dụ của tôi giđược tổ chức trong cùng một lớp để thực hiện các đại biểu của tôi. Nếu glà một vật nặng với rất nhiều tài nguyên bị bỏ lại, trình thu gom rác không thể lấy lại được, bởi vì tham chiếu trong lớp này vẫn còn tồn tại miễn là bất kỳ biểu thức lambda nào được sử dụng. Vì vậy, đây là một rò rỉ bộ nhớ tiềm năng và đó là lý do cho cảnh báo R #.

@splintor Như trong C #, các phương thức ẩn danh luôn được lưu trữ trong một lớp cho mỗi phương thức, có hai cách để tránh điều này:

  1. Sử dụng một phương thức ví dụ thay vì một phương thức ẩn danh.

  2. Chia việc tạo các biểu thức lambda thành hai phương thức.


30
Những cách có thể để tránh sự bắt giữ này là gì?
thanh nẹp

2
Cảm ơn câu trả lời tuyệt vời này - Tôi đã học được rằng có một lý do để sử dụng phương pháp không ẩn danh ngay cả khi nó được sử dụng ở một nơi duy nhất.
ScottRhee

1
@splintor Khởi tạo đối tượng bên trong ủy nhiệm hoặc chuyển nó làm tham số thay thế. Trong trường hợp trên, theo như tôi có thể nói, hành vi mong muốn thực sự là để giữ một tham chiếu đến Randomthể hiện, mặc dù.
Casey

2
@emodendroket Đúng, tại thời điểm này, chúng tôi đang nói về kiểu mã và khả năng đọc. Một lĩnh vực dễ dàng hơn để lý do về. Nếu áp lực bộ nhớ hoặc tuổi thọ đối tượng là quan trọng tôi đã chọn trường, nếu không tôi sẽ để nó ở trạng thái đóng mở ngắn gọn hơn.
yzorg

1
Trường hợp của tôi (rất nhiều) đơn giản hóa đã được đun sôi theo phương pháp nhà máy tạo ra Foo và Bar. Sau đó, nó đăng ký bắt giữ lambas vào các sự kiện được phơi bày bởi hai đối tượng đó và, thật bất ngờ, Foo giữ các hình ảnh chụp từ lamba của sự kiện Bar còn sống và ngược lại. Tôi đến từ C ++, nơi phương pháp này sẽ hoạt động tốt, và hơi ngạc nhiên khi thấy các quy tắc khác nhau ở đây. Bạn càng biết nhiều, tôi đoán.
dlf

35

Đồng ý với Peter Mortensen.

Trình biên dịch C # chỉ tạo ra một loại đóng gói tất cả các biến cho tất cả các biểu thức lambda trong một phương thức.

Ví dụ, được cung cấp mã nguồn:

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

Trình biên dịch tạo ra một loại trông như:

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

Capturephương pháp được biên dịch là:

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

Mặc dù lambda thứ hai không sử dụng x , nhưng nó không thể là rác được thu thập như xlà một thuộc tính của lớp được tạo được sử dụng trong lambda.


31

Cảnh báo là hợp lệ và được hiển thị trong các phương thức có nhiều hơn một lambda và chúng nắm bắt các giá trị khác nhau .

Khi một phương thức có chứa lambdas được gọi, một đối tượng do trình biên dịch tạo ra được khởi tạo với:

  • phương pháp ví dụ đại diện cho lambdas
  • các trường đại diện cho tất cả các giá trị được nắm bắt bởi bất kỳ lambdas nào

Ví dụ:

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

Kiểm tra mã được tạo cho lớp này (được dọn dẹp một chút):

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

Lưu ý ví dụ về LambdaHelpercác cửa hàng đã tạo cả p1p2 .

Tưởng tượng rằng:

  • callable1 giữ một tài liệu tham khảo lâu dài để tranh luận của nó, helper.Lambda1
  • callable2 không giữ một tham chiếu đến đối số của nó, helper.Lambda2

Trong tình huống này, tham chiếu đến helper.Lambda1cũng gián tiếp tham chiếu chuỗi trong p2và điều này có nghĩa là trình thu gom rác sẽ không thể giải quyết nó. Tệ nhất là rò rỉ bộ nhớ / tài nguyên. Ngoài ra, nó có thể giữ (các) đối tượng sống lâu hơn mức cần thiết, điều này có thể có tác động đến GC nếu chúng được thăng cấp từ gen0 lên gen1.


nếu chúng tôi lấy ra tài liệu tham khảo p1từ callable2như thế này: callable2(() => { p2.ToString(); });- điều này vẫn không gây ra vấn đề tương tự (người thu gom rác sẽ không thể giải quyết nó) như LambdaHelpervẫn sẽ chứa p1p2?
Antony

1
Vâng, cùng một vấn đề sẽ tồn tại. Trình biên dịch tạo một đối tượng chụp (tức là LambdaHelperở trên) cho tất cả lambdas trong phương thức cha. Vì vậy, ngay cả khi callable2không sử dụng p1, nó sẽ chia sẻ cùng một đối tượng chụp callable1và đối tượng chụp đó sẽ tham chiếu cả hai p1p2. Lưu ý rằng điều này chỉ thực sự quan trọng đối với các loại tham chiếu và p1trong ví dụ này là loại giá trị.
Drew Noakes

3

Đối với các truy vấn Linq đến Sql, bạn có thể nhận được cảnh báo này. Phạm vi của lambda có thể tồn tại lâu hơn phương thức do thực tế là truy vấn thường được hiện thực hóa sau khi phương thức nằm ngoài phạm vi. Tùy thuộc vào tình huống của bạn, bạn có thể muốn hiện thực hóa các kết quả (tức là thông qua .ToList ()) trong phương thức để cho phép GC trên các vars thể hiện của phương thức được ghi trong lambda L2S.


2

Bạn luôn có thể tìm ra lý do gợi ý R # chỉ bằng cách nhấp vào các gợi ý như được hiển thị bên dưới:

nhập mô tả hình ảnh ở đây

Gợi ý này sẽ hướng dẫn bạn ở đây .


Việc kiểm tra này thu hút sự chú ý của bạn đến thực tế là có nhiều giá trị đóng hơn đang được nắm bắt rõ ràng hơn, điều này có tác động đến tuổi thọ của các giá trị này.

Hãy xem xét các mã sau đây:

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

Trong lần đóng đầu tiên, chúng ta thấy rằng cả obj1 và obj2 đang bị bắt một cách rõ ràng; chúng ta có thể thấy điều này chỉ bằng cách nhìn vào mã. Đối với lần đóng thứ hai, chúng ta có thể thấy obj1 đang bị bắt một cách rõ ràng, nhưng ReSharper đang cảnh báo chúng ta rằng obj2 đang bị bắt ngầm.

Điều này là do một chi tiết triển khai trong trình biên dịch C #. Trong quá trình biên dịch, các bao đóng được viết lại thành các lớp với các trường chứa các giá trị đã bắt và các phương thức đại diện cho chính bao đóng. Trình biên dịch C # sẽ chỉ tạo một lớp riêng như vậy cho mỗi phương thức và nếu nhiều hơn một bao đóng được định nghĩa trong một phương thức, thì lớp này sẽ chứa nhiều phương thức, một phương thức cho mỗi bao đóng và nó cũng sẽ bao gồm tất cả các giá trị được bắt giữ từ tất cả các bao đóng.

Nếu chúng ta nhìn vào mã mà trình biên dịch tạo ra, nó trông giống như thế này (một số tên đã được xóa để dễ đọc):

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

Khi phương thức chạy, nó tạo ra lớp hiển thị, nó nắm bắt tất cả các giá trị, cho tất cả các bao đóng. Vì vậy, ngay cả khi một giá trị không được sử dụng trong một trong các bao đóng, nó vẫn sẽ bị bắt. Đây là bản chụp "ngầm" mà ReSharper đang làm nổi bật.

Hàm ý của việc kiểm tra này là giá trị đóng cửa được chụp ngầm sẽ không được thu gom rác cho đến khi chính bao đóng là rác được thu thập. Thời gian tồn tại của giá trị này hiện được gắn với thời gian đóng của một giá trị không sử dụng rõ ràng giá trị. Nếu việc đóng cửa tồn tại lâu, điều này có thể có tác động tiêu cực đến mã của bạn, đặc biệt nếu giá trị bị bắt là rất lớn.

Lưu ý rằng mặc dù đây là chi tiết triển khai của trình biên dịch, nhưng nó nhất quán giữa các phiên bản và triển khai như Microsoft (trước và sau Roslyn) hoặc trình biên dịch của Mono. Việc triển khai phải hoạt động như được mô tả để xử lý chính xác nhiều lần đóng bắt một loại giá trị. Ví dụ, nếu nhiều bao đóng bắt một int, thì chúng phải chụp cùng một thể hiện, điều này chỉ có thể xảy ra với một lớp lồng riêng được chia sẻ. Tác dụng phụ của việc này là thời gian tồn tại của tất cả các giá trị được chụp hiện là thời gian tồn tại tối đa của bất kỳ bao đóng nào nắm bắt bất kỳ giá trị nào.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.