Является ли этот объект-расширение-срок-закрытие ошибкой компилятора 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. Кажется, это исправлено :)
Леппи

2
Это также происходит в VS2008SP1. Для VS2010SP1 это происходит как для 3.5, так и для 4.0.
Леппи

5
Хм, ошибка - ужасно большое слово, которое можно применить к этому. Компилятор просто генерирует немного неэффективный код. Конечно, не утечка, этот мусор собирает без проблем. Вероятно, это было исправлено, когда они работали над асинхронной реализацией.
Ганс Пассант

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.

И во всех трех случаях метод -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сделано статичным, это также удалит ссылку? Я был бы благодарен, чтобы знать.
Ивайло Славов

1
@Ivaylo: Если бы они Foo.InstanceMethodбыли статичными, не было бы видно ни одного экземпляра, и, следовательно, не было бы никакого способа thisбыть захваченным закрытием.
Ани

1
@Ivaylo Slavov Если метод экземпляра был статическим, то поле должно быть статическим, я попробовал - и не будет «указателя».
Никлас

@Niklas, спасибо. В заключение я полагаю, что статические методы для создания лямбд гарантируют отсутствие этого ненужного указателя.
Ивайло Славов

@ Ивайло Славов, так и есть .. :)
Никлас
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.