Це помилка компілятора C #, що розширюється протягом життя, об'єкта?


136

Я відповідав на питання про можливість закриття (законно) продовження життя об'єкта, коли я зіткнувся з деяким надзвичайно цікавим кодовим кодом з боку компілятора C # (якщо це має значення 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.
Джон Скіт

15
Я не можу спростити це за допомогою попереднього перегляду VS11 Developer. Можна спростувати у VS2010SP1. Здається, це виправлено :)
leppie

2
Це також відбувається в VS2008SP1. Для VS2010SP1 це відбувається як для 3.5, так і для 4.0.
леппі

5
Хм, помилка - це жахливо велике слово, яке слід застосувати до цього. Компілятор просто генерує трохи неефективний код. Безумовно, не витік, це сміття збирається без проблем. Це, ймовірно, було виправлено, коли вони працювали над реалізацією async.
Ганс Пасант

7
@Hans, це не збирає сміття без проблем, якщо делегат пережив би термін експлуатації об'єкта, і нічого цього не заважає цього робити.
SoftMemes

Відповіді:


24

Це точно схоже на помилку. Дякую за те, що ви звернули увагу на мене Я розберуся в це. Не виключено, що воно вже знайдено та зафіксовано.


7

Це здається помилкою чи непотрібним:

Я запускаю вас, наприклад, в IL IL:

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

І в усіх 3 випадках є посилання на статичний метод, тому це робить його більш дивним. Так що після цього аналізу litle, я скажу, що його помилка / ні на що. !


Я думаю, це означає, що це BAD ідея використовувати статичні методи з батьківського класу всередині лямбда-виразу, згенерованого вкладеним класом? Мені просто цікаво, якщо Foo.InstanceMethodце зроблено статично, чи це також видалить посилання? Буду вдячний знати.
Івайло Славов

1
@Ivaylo: Якби вони Foo.InstanceMethodбули також статичними, не було б жодного примірника, і, отже, жодного способу не thisбуло захоплено закриттям.
Ані

1
@Ivaylo Slavov Якщо метод екземпляра був статичним, то поле має бути статичним, я намагався - і "цього покажчика" не буде.
Ніклас

@Niklas, Дякую На закінчення я припускаю, що статичні методи створення лямбда гарантують відсутність цього непотрібного покажчика.
Івайло Славов

@ Івайло Славов, Вгадай так .. :)
Ніклас
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.