Почему захват изменяемой структурной переменной внутри замыкания внутри оператора using изменяет ее локальное поведение? - PullRequest
20 голосов
/ 10 января 2011

Обновление : Хорошо, теперь я пошел и сделал это: я подал в Microsoft отчет об ошибке об этом, так как я серьезно сомневаюсь, что это правильное поведение. Тем не менее, я все еще не уверен на 100%, во что верить в отношении этого вопроса ; так что я могу видеть, что то, что является «правильным», открыто для некоторого уровня интерпретации.

Мне кажется, что либо Microsoft примет это за ошибку, либо ответит, что изменение переменной типа изменяемого значения в операторе using представляет собой неопределенное поведение.

Кроме того, для чего бы это ни стоило, у меня есть по крайней мере догадка относительно того, что здесь происходит. Я подозреваю, что компилятор генерирует класс для замыкания, «поднимая» локальную переменную в поле экземпляра этого класса; и поскольку он находится внутри блока using, создает поле readonly. Как указывал LukeH в комментарии к другому вопросу , это помешало бы вызовам методов, таким как MoveNext, изменять само поле (вместо этого они влияли бы на копию).


Примечание: я сократил этот вопрос для удобства чтения, хотя он еще не совсем короткий. Полный текст оригинального (более длинного) вопроса см. В истории изменений.

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

Вопрос

Если у меня есть изменяемый тип значения, который реализует IDisposable, я могу (1) вызвать метод, который изменяет состояние значения локальной переменной в операторе using, и код ведет себя так, как я ожидаю. Однако, как только я фиксирую рассматриваемую переменную в закрытии внутри в операторе using, (2) изменения значения больше не видны в локальной области действия.

Такое поведение проявляется только в случае, когда переменная захвачена внутри замыкания и в операторе using; не очевидно, когда присутствует только одно (using) или другое условие (замыкание).

Почему захват переменной типа изменяемого значения внутри замыкания в операторе using меняет ее локальное поведение?

Ниже приведены примеры кода, иллюстрирующие элементы 1 и 2. В обоих примерах будет использоваться следующая демонстрация Mutable тип значения:

struct Mutable : IDisposable
{
    int _value;
    public int Increment()
    {
        return _value++;
    }

    public void Dispose() { }
}

1. Преобразование переменной типа значения в блоке using

using (var x = new Mutable())
{
    Console.WriteLine(x.Increment());
    Console.WriteLine(x.Increment());
}

Выходной код выводит:

0
1

2. Захват переменной типа значения внутри замыкания в блоке using

using (var x = new Mutable())
{
    // x is captured inside a closure.
    Func<int> closure = () => x.Increment();

    // Now the Increment method does not appear to affect the value
    // of local variable x.
    Console.WriteLine(x.Increment());
    Console.WriteLine(x.Increment());
}

Вышеприведенный код выводит:

0
0

Дополнительные комментарии

Было отмечено, что компилятор Mono обеспечивает ожидаемое мной поведение (изменения значения локальной переменной все еще видны в случае закрытия using +). Правильно ли это поведение или нет, мне неясно.

Подробнее о моих мыслях по этому вопросу см. здесь .

Ответы [ 4 ]

11 голосов
/ 10 января 2011

Это связано с тем, как создаются и используются типы замыканий. Кажется, есть небольшая ошибка в том, как csc использует эти типы. Например, вот IL, сгенерированный gmcs Моно при вызове MoveNext ():

      IL_0051:  ldloc.3
      IL_0052:  ldflda valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> Foo/'<Main>c__AnonStorey0'::enumerator
      IL_0057:  call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()

Обратите внимание, что загружается адрес поля, что позволяет вызову метода изменить экземпляр типа значения, хранящийся в объекте замыкания. Это то, что я считаю правильным поведением, и это приводит к тому, что содержимое списка перечисляется просто отлично.

Вот что генерирует csc:

      IL_0068:  ldloc.3
      IL_0069:  ldfld valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> Tinker.Form1/'<>c__DisplayClass3'::enumerator
      IL_006e:  stloc.s 5
      IL_0070:  ldloca.s 5
      IL_0072:  call instance bool valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::MoveNext()

Так что в этом случае он берет копию экземпляра типа значения и вызывает метод для этой копии. Не должно быть удивительно, почему это никуда не приведет. Вызов get_Current () также неверен:

      IL_0052:  ldloc.3
      IL_0053:  ldfld valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> Tinker.Form1/'<>c__DisplayClass3'::enumerator
      IL_0058:  stloc.s 5
      IL_005a:  ldloca.s 5
      IL_005c:  call instance !0 valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32>::get_Current()
      IL_0061:  call void class [mscorlib]System.Console::WriteLine(int32)

Поскольку в состоянии копируемого им перечислителя не было вызвано MoveNext (), get_Current (), очевидно, возвращает default(int).

Короче говоря: csc, кажется, глючит. Интересно, что Mono понял это правильно, а MS.NET - нет!

... Я бы хотел услышать комментарии Джона Скита по поводу этой странности.


В обсуждении с Брайковичем в #mono он определил, что спецификация языка C # на самом деле не детализирует как должен быть реализован тип замыкания, или как должны получать доступы локальных объектов, захваченных в замыкании в переводе. Пример реализации в спецификации, кажется, использует метод "copy", который использует csc. Поэтому любой вывод компилятора можно считать корректным в соответствии со спецификацией языка, хотя я бы сказал, что csc должен хотя бы скопировать локальный обратно в объект замыкания после вызова метода.

7 голосов
/ 14 января 2011

Это известная ошибка;мы обнаружили это пару лет назад.Исправление может быть сломано, и проблема довольно неясна;это пункты против исправления.Поэтому он никогда не был достаточно высокоприоритетным, чтобы на самом деле это исправить.

Это было в моей очереди потенциальных тем в блоге уже пару лет;возможно, мне следует это записать.

И, между прочим, ваша догадка относительно механизма, объясняющего ошибку, абсолютно точна;там хорошая отладка экстрасенсов.

Итак, да, известная ошибка, но спасибо за отчет независимо!

0 голосов
/ 10 января 2011

Проблема в том, что перечислитель хранится в другом классе, поэтому каждое действие работает с копией перечислителя.

[CompilerGenerated]
private sealed class <>c__DisplayClass3
{
    // Fields
    public List<int>.Enumerator enumerator;

    // Methods
    public int <Main>b__1()
    {
        return this.enumerator.Current;
    }
}

public static void Main(string[] args)
{
    List<int> <>g__initLocal0 = new List<int>();
    <>g__initLocal0.Add(1);
    <>g__initLocal0.Add(2);
    <>g__initLocal0.Add(3);
    List<int> list = <>g__initLocal0;
    Func<int> CS$<>9__CachedAnonymousMethodDelegate2 = null;
    <>c__DisplayClass3 CS$<>8__locals4 = new <>c__DisplayClass3();
    CS$<>8__locals4.enumerator = list.GetEnumerator();
    try
    {
        if (CS$<>9__CachedAnonymousMethodDelegate2 == null)
        {
            CS$<>9__CachedAnonymousMethodDelegate2 = new Func<int>(CS$<>8__locals4.<Main>b__1);
        }
        while (CS$<>8__locals4.enumerator.MoveNext())
        {
            Console.WriteLine(CS$<>8__locals4.enumerator.Current);
        }
    }
    finally
    {
        CS$<>8__locals4.enumerator.Dispose();
    }
}

Без лямбды код ближе к тому, что вы ожидаете.

public static void Main(string[] args)
{
    List<int> <>g__initLocal0 = new List<int>();
    <>g__initLocal0.Add(1);
    <>g__initLocal0.Add(2);
    <>g__initLocal0.Add(3);
    List<int> list = <>g__initLocal0;
    using (List<int>.Enumerator enumerator = list.GetEnumerator())
    {
        while (enumerator.MoveNext())
        {
            Console.WriteLine(enumerator.Current);
        }
    }
}

Удельный IL

L_0058: ldfld valuetype [mscorlib]System.Collections.Generic.List`1/Enumerator<int32> Machete.Runtime.Environment/<>c__DisplayClass3::enumerator
L_005d: stloc.s CS$0$0001
L_005f: ldloca.s CS$0$0001
0 голосов
/ 10 января 2011

РЕДАКТИРОВАТЬ - Это неверно, я недостаточно внимательно прочитал вопрос.

Помещение структуры в замыкание вызывает назначение.Присвоения типов значений приводят к копированию типа.Итак, что происходит, вы создаете новый Enumerator<int>, и Current для этого перечислителя вернет 0.

using System;
using System.Collections.Generic;

class Program
{
    static void Main(string[] args)
    {
        List<int> l = new List<int>();
        Console.WriteLine(l.GetEnumerator().Current);
    }
}

Результат: 0

...