Подтвердить что ты не робот

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

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

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

Кроме того, для чего это стоит, у меня есть, по крайней мере, догадка о том, что здесь происходит. Я подозреваю, что компилятор генерирует класс для закрытия, "поднимая" локальную переменную в поле экземпляра этого класса; и поскольку он находится в блоке 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 +). Является ли это поведение правильным или нет, неясно мне.

Еще несколько моих мыслей по этому вопросу см. здесь .

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

Это связано с тем, как генерируются и используются типы закрытия. Кажется, есть тонкая ошибка в способе, которым csc использует эти типы. Например, вот IL, сгенерированный Mono 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 он определил, что спецификация языка С# на самом деле не описывает, как должен реализовываться тип замыкания, а также то, как следует переводить обращения локалей, захваченных в закрытии. Пример реализации в спецификации, похоже, использует метод "copy", который использует csc. Поэтому вывод компилятора можно считать правильным в соответствии со спецификацией языка, хотя я бы сказал, что csc должен по крайней мере скопировать локальную обратно в объект закрытия после вызова метода.

Ответ 3

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

Размещение структуры в замыкание вызывает назначение. Присвоения типов значений приводят к копированию типа. Итак, что происходит, вы создаете новый 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

Ответ 4

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

[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