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

StackOverflow https://stackoverflow.com/questions/4642665

Вопрос

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

Мое чувство состоит в том, что либо Microsoft примет, что это ошибка, либо иначе реагируют, что модификация изменяемой переменной типа Visual Value в пределах using Заявление составляет неопределенное поведение.

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


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

Я прочитал, что я считаю, являются соответствующими разделами ЭКМА-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. Захватывание переменной типа Value внутри закрытия в пределах 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 Compiler предоставляет поведение, которое я ожидаю (изменения в значение локальной переменной все еще видны в using + Закрывающий корпус). Правильно ли это поведение или нет, неясно для меня.

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

Это было полезно?

Решение

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

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

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

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

Другие советы

Это связано с тем, как генерируются типы закрытия и используются. Там, кажется, является тонкой ошибкой, так как CSC использует эти типы. Например, вот IL генерируется GMC MOSO при вызове 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 кажется багги. Интересно, что MOSO получил это правильно, пока Ms.net нет!

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


В обсуждении с Брайковием в #монов он определил, что спецификация языка C # на самом деле не детализируется как Тип закрывания должен быть реализован, ни как доступ к локалам, которые захватываются в закрытии, должны перевести. Пример реализации в спецификации, кажется, использует метод «копирования», который использует CSC. Следовательно, вывод компилятора можно считать правильным в соответствии с спецификацией языка, хотя я бы утвердовал, что CSC должен, по крайней мере, скопировать локальный обратно в объект закрытия после вызова метода.

Редактировать - это неверно, я не прочитал вопрос достаточно тщательно.

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

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

[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
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top