在迭代器的finally块中获取异常的引用

8
在一个迭代器函数或属性中允许try..finally但不允许try..catch的情况下,有没有办法在finally块内获取对异常的引用?
我不会用它来改变或干扰控制流,但仍希望能够在finally块中获取异常引用(如果有异常抛出),以便从中读取并可能添加一些数据成员
我知道由于迭代器生成类的性质,可能不可能/不允许尝试..在第一次中捕获yield语句的原因。但我仍然希望可能有某种方法(甚至是丑陋的技巧)来获得异常的控制权。
简化示例:
IEnumerable<SomeClass> Something
get
{
  try
  {
    throw new SomeException();
    yield return new SomeClass();
  }
  finally
  {
    Exception ex = ... // <= TODO - get hold of the exception here [if one was thrown]...
  }
}
4个回答

19

这是一个非常有趣的问题。

回想一下,在Linq中,提供了许多标准运算符来有效地链接在一起。目前还没有一个运算符可以让您在内部序列周围包装自定义异常处理。

因此,我的建议是编写一个新的运算符,允许您指定一个操作,以处理在执行 IEnumerator.MoveNext 期间发生的任何异常:

public static class EnumerableExceptions
{
    public static IEnumerable<TItem> Catch<TItem, TEx>(
        this IEnumerable<TItem> source, 
        Action<TEx> handler) where TEx : Exception
    {
        using (var enumerator = source.GetEnumerator())
        {
            for (; ; )
            {
                try
                {
                    if (!enumerator.MoveNext())
                        yield break;
                }
                catch (TEx x)
                {
                    handler(x);
                    yield break;
                }

                yield return enumerator.Current;
            }
        }
    }
}

假设我们现在有这个:

public class NastyException : Exception { }

public static IEnumerable<String> StringYielder()
{
    yield return "apple";
    yield return "banana";

    throw new NastyException();

    yield return "oracle";
    yield return "grapefruit";
    yield return "microsoft";
}

我们希望能够用try/catch来包装整个主体,但不幸的是这是非法的。但是我们可以包装生成的序列:

public static IEnumerable<String> LoggingStringYielder()
{
    return StringYielder().Catch(
        (NastyException ex) => 
            Console.WriteLine("Exception caught: " + ex.StackTrace));
}
那就是说,我通过调用“原始”StringYielder方法来获取一个序列,然后对其应用新的Catch运算符,指定在发生某种异常类型时要执行什么操作。这里我只打印堆栈跟踪信息。

因此,如果我这样做:

foreach (var str in LoggingStringYielder())
    Console.WriteLine(str);
程序完成而不崩溃,输出如下:
apple
banana
Exception caught:    at ConsoleApplication7.Program.<StringYielder>.. blah

虽然你不能在原始迭代器方法内部使用try catch,但是现在你可以将其“包装”在迭代器方法的外部。这就像一种非侵入式的方式,在每个yield return之间注入异常处理代码。

额外更新!

如果对我上一句话的措辞要求严格一些:

  • 首先,你可以在第一个yield return之前抛出异常,它会被视为在第一次调用MoveNext时执行的代码。因此,“每个...之间的代码”比“每个...之间的代码”更准确。

  • 其次,yield return可能接受一个需要评估的表达式,并且在评估期间可能会抛出异常。尽管从语法上看它出现在yield return之后,但应将其视为在yield return发生之前执行的代码。


5
将所有可能引发异常的代码移动到嵌套的try/catch块中,如何?
IEnumerable<int> GetFoo()
{
    for (int i = -10; i < 10; i++)
    {
        Exception ex = null;
        try
        {
            int result = 0;
            try
            {
                result = 10 / i;
            }
            catch (Exception e) // Don't normally do this!
            {
                ex = e;
                throw;
            }
            yield return result;
        }
        finally
        {
            if (ex != null)
            {
                // Use ex here
            }
        }
    }
}

然而,通过上述模式,你可能只需要在 catch 块中完成所有需要的操作,这将更加简单 - 你可能可以省去周围的 try/finally 块。


我希望我可以这样做,但这将意味着需要进行重大的重构。问题在于涉及嵌套迭代器,我正在查询一个或多个迭代器,循环遍历结果,并_yield return_一些内容(在很多地方)。虽然可以使用上述方法进行重构,但我希望通过最小化代码更改来玩得安全,以避免引入任何回归等。这就是为什么我希望只需在finally块内以某种方式获得对CLR中异常的引用... :) - KristoferA

2
在终结器期间了解是否有任何异常待处理是一项不错的能力。VB.net可以实现这一点,但比较笨拙。而C#则不行。在VB.net中,技巧如下:
  Dim PendingException as Exception = Nothing
  Try
    .. 做任何事情
    PendingException = Nothing '很重要——请参见文本
  Catch Ex As Exception When CopyFirstArgumentToSecondAndReturnFalse(Ex, PendingException)
    Throw '如果上述函数像应该那样返回false,则永远不会发生
  Finally
    '如果Try正常完成,则PendingException将为Nothing,否则将持有异常。
    Try
      .. 清理
    Catch Ex as Exception
      Throw New FailedCleanupException(Ex, PendingException)
    End Try
  End Try

请注意,如果代码由异常触发,则此技术可能非常有用,因为其保证最终会重新抛出异常或引发新的聚合异常。除其他外,如果该异常最终未被处理,"未处理的异常"调试器陷阱将在原始异常发生时触发,而不是在最后一次重新抛出时触发。这可以大大简化调试,因为调试器将可用于垃圾邮件,否则无法使用。

还要注意,在主Try块的末尾显式清除了PendingException。即使没有待处理的异常,PendingException也可能具有值。如果双重嵌套的Try块中的某些内容引发异常,而我们的try块内部不会捕获该异常,但是内部Try块的Finally子句引发了在单层嵌套的Try块中捕获的异常,则原始异常将被删除。如果PendingException为非空,则执行"CopyFirstParameterToSecondAndReturnFalse"或"PendingException = Nothing"时可能会生成特殊日志条目,因为该场景可能代表不太可能在其他地方记录的错误,但是如果有多个嵌套的catch块,则它们可能会生成冗余的日志条目。

考虑到C#不支持此方法所需的异常过滤器,编写一个VB包装器可以调用C#委托并提供必要的异常处理逻辑可能会很有帮助。

编辑 无法在try-catch块中进行yield return,但我认为这不会对以下内容造成问题:

{
  Exception ex = null;
try { CaptureExceptionInfoButDontCatch(ex,{ /* 可能引发异常的内容 */ }); yield return whatever; CaptureExceptionInfoButDontCatch(ex,{ /* 更多可能引发异常的内容 */ }); } finally { /* 如果ex为null,则表示发生了异常 */ } }

谢谢。只有一个问题:即使C#可以像VB一样做到,问题在于迭代器(特别是_yield_关键字)根本不允许使用'catch'。由于迭代器的延迟性质,包装也行不通;包装器只会返回对[编译器生成的迭代器类的实例]的引用,每当代码实际执行时,try/catch已经超出范围... - KristoferA
1
yield return 不允许在 try-catch 块内使用 "yield return",但这并不妨碍将 yield return 上下的代码包装在块中,以捕获异常而不是捕获它。请参见上面的编辑。 - supercat

1

finally块用于始终执行清理操作,因此不打算与异常交互 - 可能根本没有未决的异常。

使用带有重新抛出的catch块应该适合您:

try
{
    // .. something throws here ..
}
catch (Exception ex)
{
    // .. do whatever you need with ex here ..

    // and pass it on
    throw;
} 

1
好的,我更新了问题以反映我想要在抛出异常的情况下获取异常。不过,在yield周围是不允许使用catch的... - KristoferA

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