Дискретные методы анонимности, разделяющие класс?

Я немного играл с классом Eric Lippert Ref<T> из здесь. Я заметил в IL, что похоже, что оба анонимных метода использовали один и тот же сгенерированный класс, хотя это означало, что у класса была дополнительная переменная.

При использовании только одного определения нового класса кажется несколько разумным, мне кажется очень странным, что создается только один экземпляр <>c__DisplayClass2. Это означает, что оба экземпляра Ref<T> ссылаются на один и тот же <>c__DisplayClass2 Не означает ли это, что y невозможно собрать до тех пор, пока vart1 не будет собрано, что может произойти намного позже, чем после возврата joik? В конце концов, нет никакой гарантии, что какой-то идиот не будет писать функцию (непосредственно в IL), которая напрямую обращается к y через vart1 aftrer joik. Возможно, это может быть даже сделано с отражением, а не через сумасшедший ИЛ.

sealed class Ref<T>
{
    public delegate T Func<T>();
    private readonly Func<T> getter;
    public Ref(Func<T> getter)
    {
        this.getter = getter;
    }
    public T Value { get { return getter(); } }
}

static Ref<int> joik()
{
    int[] y = new int[50000];
    int x = 5;
    Ref<int> vart1 = new Ref<int>(delegate() { return x; });
    Ref<int[]> vart2 = new Ref<int[]>(delegate() { return y; });
    return vart1;
}

Запуск IL DASM подтвердил, что vart1 и vart2 оба использовали <>__DisplayClass2, который содержал общедоступное поле для x и y. ИЛ от joik:

.method private hidebysig static class Program/Ref`1<int32> 
        joik() cil managed
{
  // Code size       72 (0x48)
  .maxstack  3
  .locals init ([0] class Program/Ref`1<int32> vart1,
           [1] class Program/Ref`1<int32[]> vart2,
           [2] class Program/'<>c__DisplayClass2' '<>8__locals3',
           [3] class Program/Ref`1<int32> CS$1$0000)
  IL_0000:  newobj     instance void Program/'<>c__DisplayClass2'::.ctor()
  IL_0005:  stloc.2
  IL_0006:  nop
  IL_0007:  ldloc.2
  IL_0008:  ldc.i4     0xc350
  IL_000d:  newarr     [mscorlib]System.Int32
  IL_0012:  stfld      int32[] Program/'<>c__DisplayClass2'::y
  IL_0017:  ldloc.2
  IL_0018:  ldc.i4.5
  IL_0019:  stfld      int32 Program/'<>c__DisplayClass2'::x
  IL_001e:  ldloc.2
  IL_001f:  ldftn      instance int32 Program/'<>c__DisplayClass2'::'<joik>b__0'()
  IL_0025:  newobj     instance void class Program/Ref`1/Func`1<int32,int32>::.ctor(object,
                                                                                    native int)
  IL_002a:  newobj     instance void class Program/Ref`1<int32>::.ctor(class Program/Ref`1/Func`1<!0,!0>)
  IL_002f:  stloc.0
  IL_0030:  ldloc.2
  IL_0031:  ldftn      instance int32[] Program/'<>c__DisplayClass2'::'<joik>b__1'()
  IL_0037:  newobj     instance void class Program/Ref`1/Func`1<int32[],int32[]>::.ctor(object,
                                                                                        native int)
  IL_003c:  newobj     instance void class Program/Ref`1<int32[]>::.ctor(class Program/Ref`1/Func`1<!0,!0>)
  IL_0041:  stloc.1
  IL_0042:  ldloc.0
  IL_0043:  stloc.3
  IL_0044:  br.s       IL_0046
  IL_0046:  ldloc.3
  IL_0047:  ret
} // end of method Program::joik

Ответ 1

Да, реализация анонимных методов MS эффективно создает один скрытый класс на один уровень видимости, который требуется для захвата переменных и захватывает все соответствующие переменные из этой области. Я считаю, что это делается для простоты, но это действительно может увеличить время жизни некоторых объектов без необходимости.

Было бы более элегантно, если бы каждый анонимный метод мог отображать только те переменные, на которые он действительно интересовался. Однако это может значительно усложнить жизнь... если один анонимный метод захвачен x и y, один захвачен x и один захваченный y, вам понадобятся три класса: один для захвата x, один для захвата y и один для компоновки двух (но не только двух переменных). Сложный бит заключается в том, что для любого экземпляра одной переменной эта переменная должна жить в одном месте, так что все, что относится к ней, видит одно и то же значение, независимо от его изменения.

Это никак не нарушает спецификацию, но ее можно считать неудачной - я не знаю, действительно ли она укусила людей в реальной жизни, но это, безусловно, возможно.

Хорошей новостью является то, что, если команда С# решит улучшить это, они должны иметь возможность сделать это совершенно обратно совместимым способом, если только некоторые маппеты не полагаются на продление срока службы без необходимости.

Ответ 2

Джон, конечно, прав. Обычно проблема заключается в следующем:
void M()
{
    Expensive e = GetExpensive();
    Cheap c = GetCheap();
    D longLife = ()=>...c...;
    D shortLife = ()=>...e...;
    ...
}

Итак, у нас есть дорогостоящий ресурс, чья жизнь теперь зависит от времени жизни longLife, хотя shortLife собирается раньше.

Это печально, но часто. Реализации закрытий в JScript и VB имеют одинаковую проблему.

Я хотел бы решить эту проблему в гипотетической будущей версии С#, но я не гарантирую. Очевидный способ сделать это - идентифицировать классы эквивалентности замкнутых переменных, на основе которых они берутся, и генерировать классы замыкания по одному для класса эквивалентности, а не один класс замыкания.

Также могут быть вещи, которые мы могли бы сделать с анализом того, на что записаны закрытые переменные. Как отмечает Джон, в настоящее время мы ограничены нашей необходимостью фиксировать переменные, а не ценности. Мы могли бы быть более гибкими в нашей стратегии генерации кода, если бы мы идентифицировали переменные, которые никогда не записываются после создания замыкания, и превращаем их в закрытые значения, а не в закрытые переменные.