有没有办法在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]...
}
}
答案 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;
}