이 개체 수명 연장 폐쇄는 C # 컴파일러 버그입니까?


136

C # 컴파일러 (매우 중요한 경우 4.0)에서 매우 호기심 많은 코드 생성기 가 발생했을 때 객체 수명을 연장시키는 가능성에 대한 질문 에 대답하고있었습니다 (4.0).

내가 찾을 수있는 가장 짧은 재현은 다음과 같습니다.

  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 개발자 미리보기로 이것을 재현 할 수 없습니다. VS2010SP1에서 재현 할 수 있습니다. 그것은 고정 된 것 같습니다 :)
leppie

2
VS2008SP1에서도 발생합니다. VS2010SP1의 경우 3.5와 4.0 모두에서 발생합니다.
leppie

5
흠, 버그는 이것에 적용하기에 엄청나게 큰 단어입니다. 컴파일러는 약간 비효율적 인 코드를 생성합니다. 확실히 누출이 아닌이 쓰레기는 문제없이 수집됩니다. 그들이 비동기 구현을 할 때 수정되었을 것입니다.
Hans Passant

7
@Hans, 대리자가 객체의 수명 동안 살아남을 경우 문제없이 가비지 수집하지 않으며이를 막을 수있는 것이 없습니다.
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.

그리고 세 가지 경우 모두 방법 -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
    }

그리고 세 가지 경우 모두 정적 메서드에 대한 참조가 있으므로 더 이상하게 만듭니다. 그래서이 작은 분석가 후에, 나는 그 버그를 / 좋은 것으로 말할 것입니다. !


중첩 된 클래스에서 생성 된 람다 식 내부의 부모 클래스에서 정적 메서드를 사용하는 것이 나쁜 아이디어라고 생각합니다. Foo.InstanceMethod정적으로 만들어 졌는지 궁금합니다. 이것도 참조를 제거합니까? 알고 감사합니다.
Ivaylo Slavov

1
@Ivaylo : Foo.InstanceMethod정적 인 경우 에도 인스턴스가 보이지 않으므로 어떤 종류의 this클로저도 캡처 할 수 없습니다 .
애니

1
@Ivaylo Slavov 인스턴스 메소드가 정적 인 경우 필드는 정적이어야합니다. 나는 시도했습니다- '이 포인터'는 없습니다.
Niklas

@Niklas, 감사합니다. 결론적으로 나는 람다를 생성하는 정적 메소드 가이 불필요한 포인터의 부족을 보장한다고 가정합니다.
Ivaylo Slavov

@Ivaylo Slavov, 추측 .. :)
Niklas
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.