นี่เป็นข้อบกพร่องของคอมไพเลอร์ C # หรือไม่?


136

ฉันกำลังตอบคำถามเกี่ยวกับความเป็นไปได้ของการปิด (อย่างถูกกฎหมาย) การยืดอายุการใช้งานของวัตถุเมื่อฉันพบโค้ดโคเจนที่แปลกประหลาดมากในส่วนของคอมไพเลอร์ C # (4.0 ถ้าสำคัญ)

repro ที่สั้นที่สุดที่ฉันสามารถค้นหาได้มีดังต่อไปนี้:

  1. สร้างแลมบ์ดาที่จับภาพท้องถิ่นในขณะที่เรียกใช้ วิธีการคงที่ของประเภทที่มี
  2. กำหนดผู้รับมอบสิทธิ์อ้างอิงสร้างฟิลด์อินสแตนซ์ของวัตถุที่ประกอบด้วย

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

class Foo
{
    private Action _field;

    public void InstanceMethod()
    {
        var capturedVariable = Math.Pow(42, 1);

        _field = () => StaticMethod(capturedVariable);
    }

    private static void StaticMethod(double arg) { }
}

โค้ดที่สร้างจากบิลด์รีลีส (แยกเป็น 'ง่ายกว่า' C #) มีลักษณะดังนี้:

public void InstanceMethod()
{

    <>c__DisplayClass1 CS$<>8__locals2 = new <>c__DisplayClass1();

    CS$<>8__locals2.<>4__this = this; // What's this doing here?

    CS$<>8__locals2.capturedVariable = Math.Pow(42.0, 1.0);
    this._field = new Action(CS$<>8__locals2.<InstanceMethod>b__0);
}

[CompilerGenerated]
private sealed class <>c__DisplayClass1
{
    // Fields
    public Foo <>4__this; // Never read, only written to.
    public double capturedVariable;

    // Methods
    public void <InstanceMethod>b__0()
    {
        Foo.StaticMethod(this.capturedVariable);
    }
}

สังเกตว่า <>4__thisฟิลด์ของวัตถุการปิดนั้นบรรจุด้วยการอ้างอิงวัตถุ แต่ไม่เคยอ่านจาก (ไม่มีเหตุผล)

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


19
น่าสนใจ ดูเหมือนว่าเป็นข้อบกพร่องสำหรับฉัน โปรดทราบว่าถ้าคุณไม่ได้กำหนดให้สนามอินสแตนซ์ (เช่นถ้าคุณกลับค่า) ก็ไม่ได้thisจับ
Jon Skeet

15
ฉันไม่สามารถทำซ้ำสิ่งนี้ด้วยการดูตัวอย่าง VS11 Developer สามารถทำซ้ำใน VS2010SP1 ดูเหมือนว่ามันจะได้รับการแก้ไข :)
leppie

2
สิ่งนี้ก็เกิดขึ้นใน VS2008SP1 สำหรับ VS2010SP1 จะเกิดขึ้นกับทั้ง 3.5 และ 4.0
leppie

5
อืมข้อผิดพลาดเป็นคำที่ยิ่งใหญ่มากที่จะนำไปใช้กับสิ่งนี้ คอมไพเลอร์เพิ่งสร้างรหัสที่ไม่มีประสิทธิภาพเล็กน้อย ไม่รั่วแน่นอนขยะนี้รวบรวมได้โดยไม่มีปัญหา มันอาจได้รับการแก้ไขเมื่อพวกเขาทำงานในการปรับใช้ async
Hans Passant

7
@ ฮันนี่จะไม่เก็บขยะโดยไม่มีปัญหาหากผู้รับมอบสิทธิ์จะอยู่รอดตลอดชีวิตของวัตถุและไม่มีอะไรป้องกันไม่ให้เกิดขึ้น
SoftMemes

คำตอบ:


24

แน่นอนว่าดูเหมือนว่าเป็นข้อบกพร่อง ขอขอบคุณที่แจ้งให้ฉันทราบ ฉันจะดูมัน เป็นไปได้ว่ามันถูกค้นพบและแก้ไขแล้ว


7

ดูเหมือนว่าจะเป็นข้อบกพร่องหรือไม่จำเป็น:

ฉันให้คุณเป็นตัวอย่างใน IL lang:

.method public hidebysig 
    instance void InstanceMethod () cil managed 
{
    // Method begins at RVA 0x2074
    // Code size 63 (0x3f)
    .maxstack 4
    .locals init (
        [0] class ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'   'CS$<>8__locals2'
    )

    IL_0000: newobj instance void ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::.ctor()
    IL_0005: stloc.0
    IL_0006: ldloc.0
    IL_0007: ldarg.0
    IL_0008: stfld class ConsoleApplication1.Program/Foo ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::'<>4__this' //Make ref to this
    IL_000d: nop
    IL_000e: ldloc.0
    IL_000f: ldc.r8 42
    IL_0018: ldc.r8 1
    IL_0021: call float64 [mscorlib]System.Math::Pow(float64, float64)
    IL_0026: stfld float64 ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::capturedVariable
    IL_002b: ldarg.0
    IL_002c: ldloc.0
    IL_002d: ldftn instance void ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::'<InstanceMethod>b__0'()
    IL_0033: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
    IL_0038: stfld class [mscorlib]System.Action ConsoleApplication1.Program/Foo::_field
    IL_003d: nop
    IL_003e: ret
} // end of method Foo::InstanceMethod

ตัวอย่างที่ 2:

class Program
{
    static void Main(string[] args)
    {
    }


    class Foo
    {
        private Action _field;

        public void InstanceMethod()
        {
            var capturedVariable = Math.Pow(42, 1);

            _field = () => Foo2.StaticMethod(capturedVariable);  //Foo2

        }

        private static void StaticMethod(double arg) { }
    }

    class Foo2
    {

        internal static void StaticMethod(double arg) { }
    }


}

ใน cl: (หมายเหตุ !! ตอนนี้การอ้างอิงนี้หายไป!)

public hidebysig 
        instance void InstanceMethod () cil managed 
    {
        // Method begins at RVA 0x2074
        // Code size 56 (0x38)
        .maxstack 4
        .locals init (
            [0] class ConsoleApplication1.Program/Foo/'<>c__DisplayClass1' 'CS$<>8__locals2'
        )

        IL_0000: newobj instance void ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::.ctor()
        IL_0005: stloc.0
        IL_0006: nop //No this pointer
        IL_0007: ldloc.0
        IL_0008: ldc.r8 42
        IL_0011: ldc.r8 1
        IL_001a: call float64 [mscorlib]System.Math::Pow(float64, float64)
        IL_001f: stfld float64 ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::capturedVariable
        IL_0024: ldarg.0 //No This ref
        IL_0025: ldloc.0
        IL_0026: ldftn instance void ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::'<InstanceMethod>b__0'()
        IL_002c: newobj instance void [mscorlib]System.Action::.ctor(object, native int)
        IL_0031: stfld class [mscorlib]System.Action ConsoleApplication1.Program/Foo::_field
        IL_0036: nop
        IL_0037: ret
    }

ตัวอย่าง 3:

class Program
{
    static void Main(string[] args)
    {
    }

    static void Test(double arg)
    {

    }

    class Foo
    {
        private Action _field;

        public void InstanceMethod()
        {
            var capturedVariable = Math.Pow(42, 1);

            _field = () => Test(capturedVariable);  

        }

        private static void StaticMethod(double arg) { }
    }


}

ใน IL: (ตัวชี้นี้กลับมา)

IL_0006: ldloc.0
IL_0007: ldarg.0
IL_0008: stfld class ConsoleApplication1.Program/Foo ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::'<>4__this' //Back again.

และในทั้งสามกรณี method-b__0 () - มีลักษณะเหมือนกัน:

instance void '<InstanceMethod>b__0' () cil managed 
    {
        // Method begins at RVA 0x2066
        // Code size 13 (0xd)
        .maxstack 8

        IL_0000: ldarg.0
        IL_0001: ldfld float64 ConsoleApplication1.Program/Foo/'<>c__DisplayClass1'::capturedVariable
                   IL_0006: call void ConsoleApplication1.Program/Foo::StaticMethod(float64) //Your example
                    IL_0006: call void ConsoleApplication1.Program/Foo2::StaticMethod(float64)//Example 2
        IL_0006: call void ConsoleApplication1.Program::Test(float64) //Example 3
        IL_000b: nop
        IL_000c: ret
    }

และในทั้ง 3 กรณีมีการอ้างอิงถึงวิธีสแตติกดังนั้นมันจึงแปลกกว่า ดังนั้นหลังจาก analys litle นี้ฉันจะบอกว่ามันเป็นข้อผิดพลาด / ไม่ดี !


ฉันคิดว่านี่หมายความว่ามันเป็นความคิดที่ไม่ดีที่จะใช้วิธีการคงที่จากระดับผู้ปกครองภายในการแสดงออกแลมบ์ดาที่สร้างขึ้นโดยระดับที่ซ้อนกัน? ฉันแค่สงสัยว่าถ้าFoo.InstanceMethodทำคงที่นี้จะลบอ้างอิงหรือไม่ ฉันจะขอบคุณที่รู้
Ivaylo Slavov

1
@Ivaylo: ถ้าFoo.InstanceMethodเป็นแบบคงที่จะไม่มีตัวอย่างในการมองเห็นและดังนั้นจึงไม่มีวิธีใดที่thisจะถูกจับโดยการปิด
Ani

1
@Ivaylo Slavov หากวิธีการแบบอินสแตนซ์เป็นแบบคงที่แล้วฟิลด์จะต้องคงที่ฉันได้ลอง - และจะไม่มี 'ตัวชี้นี้'
Niklas

@Niklas ขอบคุณ โดยสรุปฉันคิดว่าวิธีการคงที่สำหรับการสร้าง lambdas จะรับประกันการขาดตัวชี้ที่ไม่จำเป็นนี้
Ivaylo Slavov

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