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

时间:2011-01-17 07:26:33

标签: c# ienumerable

有没有办法在iterator function or property that allow try..finally but not try..catch的finally块中引用异常?

我不打算用它来改变或搞乱控制流,但是希望能够在finally块中获得对异常的引用(如果有人被抛出),以便从中读取它可能会添加内容到Data member

据我所知,由于编译器从迭代器生成类的性质,可能不可能/允许出于同样的原因,首先不允许在yield语句周围使用try..catch。但我仍然希望有一些方法(甚至是丑陋的技巧)来控制异常。

简化示例:

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 个答案:

答案 0 :(得分:18)

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

回想一下,在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

因此,虽然你不能在原始迭代器方法中尝试捕获代码,但现在可以将它“包装”在迭代器方法的外部。这就像是一种非侵入式的方式,在每个yield return 之间的代码周围注入异常处理。

奖励更新!

对我最后一句话的措辞非常挑剔:

  • 首先,您可以在第一个yield return之前抛出,并且它的处理方式相同,因为该代码在第一次调用MoveNext时执行。所以“... 之前的代码 ......”比“...... 之间的代码”更准确。“

  • 其次,yield return可以接受必须进行评估的表达式,并且可以在评估期间抛出。这应该被视为在yield return发生之前执行的代码,即使它在语法之后出现。

答案 1 :(得分: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块。

答案 2 :(得分:2)

在终结器中了解待处理的异常(如果有)是一种很好的能力。 VB.net使它成为可能,尽管很尴尬。 C#没有。在vb.net中,技术是:

  Dim PendingException as Exception = Nothing
  Try
    .. Do Whatever
    PendingException = Nothing ' Important -- See text
  Catch Ex As Exception When CopyFirstArgumentToSecondAndReturnFalse(Ex, PendingException)
    Throw ' Should never happen if above function returns false like it should
  Finally
    ' PendingException will be Nothing if Try completed normally, or hold exception if not.
    Try
      .. Cleanup
    Catch Ex as Exception
      Throw New FailedCleanupException(Ex, PendingException)
    End Try
  End Try

请注意,如果保证由异常触发的代码最终重新抛出异常或抛出新的聚合异常,这可能是一种非常有用的技术。除其他事项外,如果异常最终未被处理,则在原始异常发生时将触发“未处理异常”调试器陷阱,而不是上次重新抛出异常。这可以大大简化调试,因为调试器可以使用很多程序状态,否则会被破坏。

另请注意,PendingException在主Try块的末尾显式清除。即使没有挂起的异常,PendingException也可能具有值。如果双重嵌套的Try块中的某些内容抛出一个异常而不会在我们的try块中捕获,则可能会发生这种情况,但内部Try块的Finally子句抛出异常,该异常在单独嵌套的Try块中捕获。在这种情况下,原始异常实际上消失了。如果在PendingException为非null时执行“CopyFirstParameterToSecondAndReturnFalse”或“PendingException = Nothing”,则生成特殊日志条目可能会很好,因为该场景可能代表一个不容易在其他任何地方记录的错误,但是如果有多个嵌套的catch块,它们可以生成冗余的日志条目。

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

修改的 在try-catch块中不能进行yield return,但是我不会看到这会导致类似的问题:

{
  Exception ex = null;

  try
  {
    CaptureExceptionInfoButDontCatch(ex,{
       /* Stuff that might throw */
    });
    yield return whatever;
    CaptureExceptionInfoButDontCatch(ex,{
       /* More that might throw */
    });
  }
  finally
  {
    /* If ex is nothing, exception occurred */
  }
}

答案 3 :(得分:1)

finally块用于始终执行的清理,因此不打算用异常进行操作 - 根本不存在待处理的异常。

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

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

    // and pass it on
    throw;
}