C#中的try-finally语句在迭代器中是否会破坏CERs?

18

显然,约束执行区域保证并不适用于迭代器(可能是因为它们的实现方式),但这是一个错误还是设计如此?[请参见下面的示例。]

即,使用约束执行区域和迭代器的规则是什么?

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]; }
}

(代码大部分是从这里偷来的。)


2
就这个问题而言,你似乎是第一个注意到的人......至少从我搜索其他相关参考内容来看是这样的。 - Brian Gideon
我在 https://vmccontroller.svn.codeplex.com/svn/VmcController/VmcServices/DetectOpenFiles.cs 找到了这段代码片段,其中毫不知情的作者将无法获得他认为自己得到的 CER。 - Brian Gideon
@Brian:哈哈,不错。我认为大多数人很少使用它,而那些使用它的人可能已经直觉地知道了,而没有真正想过它。虽然这只是我的猜测。 - user541686
我不知道。你的发现相当深奥。可能那些从事CER或迭代器工作的人毕竟没有考虑过这种边缘情况。否则,你可能会期望得到一个编译器错误或警告,就像在尝试将yield return放入try-catch中时所获得的那样。仅此而已…… - Brian Gideon
1个回答

15

我不知道这是一个bug还是一个极其奇怪的边缘案例,CERs没有被设计来处理这种情况。

下面是相关的代码:

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

当这个被编译后,我们尝试使用反编译工具Reflector将其转换为C#代码,我们得到了以下结果。

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

等一下!Reflector搞错了,实际的IL代码应该是这样的。

.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中显示存在该调用。那它到底在哪里呢?嗯,在自动生成的IEnumeratorMoveNext方法中就在那里。这次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();
}
所以让我们更仔细地检查一下。我们现在将PrepareConstrainedRegions调用放在了try块内,而不是应该放在外面的位置。而我们的StackOverflow调用已经从finally块移动到了try块中。
根据文档所述,PrepareConstrainedRegions必须紧接着try块之前。因此,假设如果放在其他任何地方都是无效的。
但是,即使C#编译器没有出错,事情仍然会出错,因为try块不受限制。只有catchfinallyfault块是受限制的。你猜怎么着?那个StackOverflow调用被从finally块移动到了try块中!

+1,回答很好,但是fault块是什么?编辑:没关系,它与yield有关。 - Jalal Said
1
@Jalal:不,这与yield无关。它基本上只是catch { ... throw; },没有额外的throw语句。(因为它们几乎是相同的东西,所以这不是C#的一个特性。) - user541686
1
@Jalal 故障块类似于 finally 块,但只在由于异常而离开时运行。有关更多信息,请参见此链接:http://www.simple-talk.com/community/blogs/simonc/archive/2011/02/09/99250.aspx编译器在枚举状态机的实现中使用它,但它并不特定于 yield 关键字。 - Chris Hannon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接