เหตุใด 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

1
คุณอาจเห็นสิ่งนี้หากคุณกำหนดรายการนอกการลอง / จับและทำทุกอย่างที่คุณเพิ่มในการลอง / จับแล้วตั้งค่าผลลัพธ์เป็นวัตถุอื่น การย้ายการกำหนด / การเพิ่มภายในการลอง / จับจะช่วยให้ GC หวังว่านี่จะสมเหตุสมผล
คา Montoya

คำตอบ:


391

คำเตือนจะบอกคุณว่าตัวแปร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" ที่แลมบ์ด้าแรก มันบอกฉันว่าgไม่สามารถเก็บขยะได้ตราบใดที่แลมบ์ดาแรกใช้งานอยู่

คอมไพเลอร์สร้างคลาสสำหรับนิพจน์แลมบ์ดาและวางตัวแปรทั้งหมดในคลาสนั้นซึ่งใช้ในนิพจน์แลมบ์ดา

ดังนั้นในตัวอย่างของฉันgและiอยู่ในระดับเดียวกันสำหรับการดำเนินการของผู้ได้รับมอบหมายของฉัน ถ้าgเป็นวัตถุหนักที่มีทรัพยากรจำนวนมากทิ้งตัวเก็บขยะไม่สามารถเรียกคืนได้เพราะการอ้างอิงในคลาสนี้ยังมีชีวิตอยู่ตราบใดที่มีการใช้แลมบ์ดานิพจน์อยู่ ดังนั้นนี่คือการรั่วไหลของหน่วยความจำที่มีศักยภาพและนั่นคือเหตุผลสำหรับการเตือน R #

@splintor เช่นเดียวกับใน C # วิธีนิรนามจะถูกเก็บไว้ในคลาสเดียวต่อวิธีมีสองวิธีในการหลีกเลี่ยง:

  1. ใช้วิธีการอินสแตนซ์แทนวิธีไม่ระบุชื่อ

  2. แบ่งการสร้างแลมบ์ดานิพจน์เป็นสองวิธี


30
มีวิธีใดบ้างที่สามารถหลีกเลี่ยงการดักจับนี้
splintor

2
ขอบคุณสำหรับคำตอบที่ยอดเยี่ยมนี้ฉันได้เรียนรู้ว่ามีเหตุผลที่จะใช้วิธีที่ไม่ระบุตัวตนแม้ว่าจะใช้ในที่เดียวก็ตาม
ScottRhee

1
@splintor ยกตัวอย่างวัตถุภายในผู้รับมอบสิทธิ์หรือส่งเป็นพารามิเตอร์แทน ในกรณีข้างต้นเท่าที่ฉันสามารถบอกได้ว่าพฤติกรรมที่ต้องการนั้นจริง ๆ แล้วมีการอ้างอิงถึงRandomอินสแตนซ์
Casey

2
@emodendroket ถูกต้อง ณ จุดนี้เรากำลังพูดถึงสไตล์โค้ดและความสามารถในการอ่าน เขตข้อมูลมีเหตุผลง่ายกว่า หากความกดดันของหน่วยความจำหรืออายุการใช้งานของวัตถุมีความสำคัญฉันจะต้องเลือกฟิลด์มิฉะนั้นฉันจะปล่อยให้มันปิดอย่างรัดกุม
yzorg

1
เคสของฉันเรียบง่าย (มาก) ต้มจนเป็นวิธีโรงงานที่สร้าง Foo และ Bar จากนั้นจะสมัครสมาชิกจับภาพ lambas กับกิจกรรมที่เปิดเผยโดยวัตถุสองชิ้นนี้และแปลกใจอย่างไม่น่าเชื่อ Foo ช่วยให้จับได้จาก lamba เหตุการณ์ในบาร์ที่ยังมีชีวิตอยู่และในทางกลับกัน ฉันมาจาก C ++ ซึ่งวิธีการนี้ใช้ได้ผลดีและรู้สึกประหลาดใจเล็กน้อยที่พบว่ากฎแตกต่างไปจากที่นี่ ยิ่งฉันรู้มากเท่าไหร่
dlf

35

เห็นด้วยกับปีเตอร์มอร์เทนเซ่น

คอมไพเลอร์ C # สร้างประเภทเดียวเท่านั้นที่แค็ปซูลตัวแปรทั้งหมดสำหรับการแสดงออกแลมบ์ดาทั้งหมดในวิธีการ

ตัวอย่างเช่นให้รหัสแหล่งที่มา:

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

แม้ว่าแลมบ์ดาที่สองจะไม่ใช้งานxแต่ก็ไม่สามารถรวบรวมขยะตามที่xรวบรวมเป็นคุณสมบัติของคลาสที่สร้างขึ้นที่ใช้ในแลมบ์ดา


31

คำเตือนนั้นถูกต้องและแสดงในวิธีการที่มีแลมบ์ดามากกว่าหนึ่งแลปและจะเก็บค่าต่างๆ ได้

เมื่อเมธอดที่มี lambdas ถูกเรียกใช้วัตถุที่คอมไพเลอร์สร้างขึ้นจะถูกสร้างอินสแตนซ์ด้วย:

  • วิธีการอินสแตนซ์ที่เป็นตัวแทนของ lambdas
  • สาขาคิดเป็นค่าที่ถูกเก็บใด ๆของผู้ lambdas

ตัวอย่างเช่น:

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ร้านค้าที่สร้างขึ้นทั้งp1และp2และ

ลองจินตนาการว่า:

  • callable1 เก็บการอ้างอิงที่ยาวนานของอาร์กิวเมนต์ helper.Lambda1
  • callable2 ไม่ได้อ้างอิงถึงการโต้แย้ง helper.Lambda2

ในสถานการณ์นี้การอ้างอิงไปhelper.Lambda1ยังการอ้างอิงสายอักขระทางอ้อมp2และนี่หมายความว่าตัวรวบรวมขยะจะไม่สามารถ deallocate ที่แย่ที่สุดคือหน่วยความจำ / ทรัพยากรรั่วไหล อีกทางหนึ่งอาจทำให้วัตถุมีอายุการใช้งานนานกว่าที่กำหนดซึ่งอาจส่งผลกระทบต่อ GC หากได้รับการเลื่อนระดับจาก gen0 ถึง gen1


ถ้าเราเอาออกอ้างอิงของp1จากcallable2เช่นนี้callable2(() => { p2.ToString(); });- จะยังคงไม่ก่อให้เกิดปัญหาเดียวกัน (เก็บขยะจะไม่สามารถที่จะ deallocate มัน) ในขณะที่LambdaHelperจะยังคงมีp1และp2?
Antony

1
ใช่ปัญหาเดียวกันจะมีอยู่ คอมไพเลอร์สร้างหนึ่งวัตถุการจับภาพ (เช่นLambdaHelperด้านบน) สำหรับ lambdas ทั้งหมดภายในวิธีการหลัก ดังนั้นแม้ว่าcallable2ได้ใช้ไม่มีp1ก็จะแบ่งปันวัตถุจับเดียวกับcallable1และว่าวัตถุจับจะอ้างอิงทั้งสองและp1 p2โปรดทราบว่าสิ่งนี้สำคัญสำหรับประเภทการอ้างอิงเท่านั้นและp1ในตัวอย่างนี้เป็นประเภทค่า
Drew Noakes

3

สำหรับข้อความค้นหา Linq to Sql คุณอาจได้รับคำเตือนนี้ ขอบเขตของแลมบ์ดาอาจสูงกว่าวิธีการเนื่องจากข้อเท็จจริงที่ว่าแบบสอบถามมักเกิดขึ้นจริงหลังจากวิธีนั้นอยู่นอกขอบเขต ขึ้นอยู่กับสถานการณ์ของคุณคุณอาจต้องการทำให้ผลลัพธ์เป็นจริง (เช่นผ่าน. ToList ()) ภายในวิธีการอนุญาตให้ GC สำหรับ vars อินสแตนซ์ของวิธีการที่ถ่ายในแลมบ์ดา L2S


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 นั้นถูกจับอย่างชัดเจน เราสามารถเห็นสิ่งนี้ได้เพียงแค่ดูรหัส สำหรับการปิดครั้งที่สองเราจะเห็นว่า obj1 นั้นถูกจับภาพไว้อย่างชัดเจน แต่ ReSharper เตือนเราว่า obj2 นั้นถูกจับโดยปริยาย

นี่เป็นเพราะรายละเอียดการใช้งานในคอมไพเลอร์ C # ในระหว่างการรวบรวมการปิดจะถูกเขียนใหม่ในคลาสที่มีเขตข้อมูลที่เก็บค่าที่บันทึกไว้และวิธีการที่แสดงถึงการปิดตัวเอง คอมไพเลอร์ 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);
    }
}

เมื่อวิธีการทำงานมันจะสร้างคลาสการแสดงผลซึ่งจับค่าทั้งหมดสำหรับการปิดทั้งหมด ดังนั้นแม้ว่าจะไม่ได้ใช้ค่าในการปิดอย่างใดอย่างหนึ่ง แต่ก็จะยังคงถูกจับ นี่คือการจับ "โดยนัย" ที่ ReSharper กำลังเน้น

ความหมายของการตรวจสอบนี้คือค่าการปิดที่จับได้โดยปริยายจะไม่ถูกเก็บขยะจนกว่าการปิดตัวเองจะเป็นการเก็บขยะ อายุการใช้งานของค่านี้จะเชื่อมโยงกับอายุการใช้งานของการปิดที่ไม่ได้ใช้ค่าอย่างชัดเจน หากการปิดนั้นใช้เวลานานนี่อาจมีผลเสียกับรหัสของคุณโดยเฉพาะถ้าค่าที่บันทึกไว้มีขนาดใหญ่มาก

โปรดทราบว่าแม้ว่านี่จะเป็นรายละเอียดการใช้งานของคอมไพเลอร์ แต่ก็มีความสอดคล้องกันระหว่างเวอร์ชันและการใช้งานเช่น Microsoft (ก่อนและหลังโรสลิน) หรือคอมไพเลอร์ของโมโน การใช้งานจะต้องทำงานตามที่อธิบายไว้เพื่อให้สามารถจัดการประเภทการปิดหลายรายการได้อย่างถูกต้อง ตัวอย่างเช่นหากการปิดหลายรายการจับ int แล้วพวกเขาจะต้องจับอินสแตนซ์เดียวกันซึ่งสามารถเกิดขึ้นได้กับคลาสที่ซ้อนกันไพรเวตส่วนตัวเดียว ผลข้างเคียงของสิ่งนี้คือว่าอายุการใช้งานของค่าที่บันทึกไว้ทั้งหมดจะเป็นอายุการใช้งานสูงสุดของการปิดใด ๆ ที่จับค่าใด ๆ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.