Нарушают ли C# try-finally CER в итераторах?

Судя по всему, гарантии Constrained Execution Region не распространяются на итераторы (вероятно, из-за того, как они реализованы и все такое), но это баг или так задумано? [См. пример ниже.]

т. е. каковы правила использования CER с итераторами?

using System.Runtime.CompilerServices;
using System.Runtime.ConstrainedExecution;

class Program
{
    static bool cerWorked;
    static void Main(string[] args)
    {
        try
        {
            cerWorked = true;
            foreach (var v in Iterate()) { }
        }
        catch { System.Console.WriteLine(cerWorked); }
        System.Console.ReadKey();
    }

    [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
    unsafe static void StackOverflow()
    {
        Big big;
        big.Bytes[int.MaxValue - 1] = 1;
    }

    static System.Collections.Generic.IEnumerable<int> Iterate()
    {
        RuntimeHelpers.PrepareConstrainedRegions();
        try { cerWorked = false; yield return 5; }
        finally { StackOverflow(); }
    }

    unsafe struct Big { public fixed byte Bytes[int.MaxValue]; }
}

(Код в основном украден из здесь .)


person user541686    schedule 28.07.2011    source источник
comment
Как бы то ни было, вы, кажется, первый, кто это заметил... по крайней мере, насколько я мог судить по поиску в Google других упоминаний об этом.   -  person Brian Gideon    schedule 28.07.2011
comment
Я нашел это vmccontroller.svn.codeplex.com/svn/VmcController/ VmcServices/ фрагмент кода, в котором ничего не подозревающий автор не получит CER, который, по его мнению, он получит.   -  person Brian Gideon    schedule 28.07.2011
comment
@ Брайан: Лол, мило. Я думаю, что это то, что большинство людей не используют очень часто, а те, кто использует, вероятно, уже знают это интуитивно, даже не задумываясь об этом. Хотя только мое предположение.   -  person user541686    schedule 28.07.2011
comment
Я не знаю. Ваше открытие довольно эзотерично. Люди, которые работали над CER или итераторами, возможно, вообще не думали об этом пограничном случае. В противном случае вы можете ожидать ошибку или предупреждение компилятора, как вы получаете, когда пытаетесь поместить yield return в try-catch. Просто говорю...   -  person Brian Gideon    schedule 29.07.2011


Ответы (1)


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

Итак, вот соответствующий код.

private static IEnumerable<int> Iterate()
{
    RuntimeHelpers.PrepareConstrainedRegions();
    try { cerWorked = false; yield return 5; }
    finally { StackOverflow(); }
}

Когда это компилируется и мы пытаемся декомпилировать его в C# с помощью Reflector, мы получаем это.

private static IEnumerable<int> Iterate()
{
    RuntimeHelpers.PrepareConstrainedRegions();
    cerWorked = false;
    yield return 5;
}

Теперь подождите секунду! Рефлектор все испортил. Вот так на самом деле выглядит ИЖ.

.method private hidebysig static class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> Iterate() cil managed
{
    .maxstack 2
    .locals init (
        [0] class Sandbox.Program/<Iterate>d__1 d__,
        [1] class [mscorlib]System.Collections.Generic.IEnumerable`1<int32> enumerable)
    L_0000: ldc.i4.s -2
    L_0002: newobj instance void Sandbox.Program/<Iterate>d__1::.ctor(int32)
    L_0007: stloc.0 
    L_0008: ldloc.0 
    L_0009: stloc.1 
    L_000a: br.s L_000c
    L_000c: ldloc.1 
    L_000d: ret 
}

Обратите внимание, что на самом деле никакого вызова PrepareConstrainedRegions нет, несмотря на то, что говорит Reflector. Так где же он прячется? Ну, это прямо там, в автоматически сгенерированном методе IEnumerator MoveNext. На этот раз Reflector делает все правильно.

private bool MoveNext()
{
    try
    {
        switch (this.<>1__state)
        {
            case 0:
                this.<>1__state = -1;
                RuntimeHelpers.PrepareConstrainedRegions();
                this.<>1__state = 1;
                Program.cerWorked = false;
                this.<>2__current = 5;
                this.<>1__state = 2;
                return true;

            case 2:
                this.<>1__state = 1;
                this.<>m__Finally2();
                break;
        }
        return false;
    }
    fault
    {
        this.System.IDisposable.Dispose();
    }
}

И куда таинственно переместился этот призыв к StackOverflow? Прямо внутри метода m_Finally2().

private void <>m__Finally2()
{
    this.<>1__state = -1;
    Program.StackOverflow();
}

Итак, давайте рассмотрим это немного подробнее. Теперь у нас есть вызов PrepareConstainedRegions внутри блока try, а не снаружи, где он должен быть. И наш вызов StackOverflow переместился из блока finally в блок try.

Согласно документации, PrepareConstrainedRegions должен стоять непосредственно перед блоком try. Таким образом, предполагается, что он неэффективен, если его разместить где-либо еще.

Но даже если бы компилятор C# понял эту часть правильно, все равно все было бы испорчено, потому что блоки try не ограничены. Доступны только блоки catch, finally и fault. И угадайте, что? Этот вызов StackOverflow был перемещен из блока finally в блок try!

person Brian Gideon    schedule 28.07.2011
comment
+1, хороший ответ, но что такое блок fault? Изменить: неважно, что это связано с доходность - person Jalal Said; 28.07.2011
comment
@Jalal: Нет, это не связано с доходностью. Это просто catch { ... throw; } без дополнительного оператора throw. (Поскольку это почти одно и то же, это не особенность C#.) - person user541686; 28.07.2011
comment
@Jalal Блок сбоя похож на блок finally, но запускается только тогда, когда управление уходит из-за исключения. Подробнее об этом . Компилятор использует его в своей реализации перечислимого конечного автомата, но это не относится к ключевому слову yield. - person Chris Hannon; 28.07.2011