包装IEnumerable并捕获exception

我有一堆可以Process()对象的类,并返回自己的对象:

 public override IEnumerable Process(IEnumerable incoming) { ... } 

我想编写一个可以包装其中一个处理器的处理器类,并记录包装的Process()方法可能抛出的任何未捕获的exception。 我的第一个想法是这样的:

 public override IEnumerable Process(IEnumerable incoming) { try { foreach (var x in this.processor.Process(incoming)) { yield return x; } } catch (Exception e) { WriteToLog(e); throw; } } 

但这不起作用,因为CS1626:不能在带有catch子句的try块的主体中​​产生值 。

所以我想写一些概念上等同但编译的东西。 :-)我有这个:

 public override IEnumerable Process(IEnumerable incoming) { IEnumerator walker; try { walker = this.processor.Process(incoming).GetEnumerator(); } catch (Exception e) { WriteToLog(e); throw; } while (true) { T value; try { if (!walker.MoveNext()) { break; } value = walker.Current; } catch (Exception e) { WriteToLog(e); throw; } yield return value; } } 

但这比我希望的更复杂,而且我不完全确定它的正确性还是没有更简单的方法。

我在这里走在正确的轨道上吗? 有没有更简单的方法?

可以编写linq扩展以跳过导致exception的所有元素并将其传递给日志记录函数。

  public static IEnumerable CatchExceptions (this IEnumerable src, Action action = null) { using (var enumerator = src.GetEnumerator()) { bool next = true; while (next) { try { next = enumerator.MoveNext(); } catch (Exception ex) { if (action != null) { action(ex); } continue; } if (next) { yield return enumerator.Current; } } } } 

例:

 ienumerable.Select(e => e.something).CatchExceptions().ToArray() ienumerable.Select(e => e.something).CatchExceptions((ex) => Logger.Log(ex, "something failed")).ToArray() 

如果您想要做的是在处理枚举结果期间处理exception,那么您尝试逻辑只需要直接进入for / while循环。

但是您的示例读取就好像您正在尝试捕获并跳过枚举提供程序引发的exception。

据我所知,C#中没有办法迭代枚举器并跳过枚举器本身内发生的exception。 如果枚举器引发exception,则将来对MoveNext()的所有调用都将导致错误输出。

解释原因的最简单方法是使用这个非常简单的枚举:

 IEnumerable TestCases() { yield return 1; yield return 2; throw new ApplicationException("fail eunmeration"); yield return 3; yield return 4; } 

可以理解的是,当我们查看这个示例时,很明显抛出的exception会导致整个块退出,并且不会处理第3和第4个yield语句。 实际上,在第3个yield语句中获得通常的“无法访问的代码检测”编译器警告。

因此,当可枚举更复杂时,适用相同的规则:

 IEnumerable TestCases2() { foreach (var item in Enumerable.Range(0,10)) { switch(item) { case 2: case 5: throw new ApplicationException("This bit failed"); default: yield return item; break; } } } 

引发exception时,此块的处理将停止并将调用堆栈传递回最近的exception处理程序。

我在SO上找到的解决这个问题的所有可行示例都不会进入枚举中的下一个项目,它们都会在第一个exception时中断

因此,要在枚举中跳过例外,您需要提供者来促进它。 这只有在您编写提供程序时才可能实现,或者您可以联系开发人员,以下是如何实现此目的的过度简化示例:

 IEnumerable TestCases3(Action exceptionHandler) { foreach (var item in Enumerable.Range(0, 10)) { int value = default(int); try { switch (item) { case 2: case 5: throw new ApplicationException("This bit failed"); default: value = item; break; } } catch(Exception e) { if (exceptionHandler != null) { exceptionHandler(item, e); continue; } else throw; } yield return value; } } 

 foreach (var item in TestCases3( (int item, Exception ex) => Console.Out.WriteLine("Error on item: {0}, Exception: {1}", item, ex.Message))) { Console.Out.WriteLine(item); } 

这将产生以下输出:

 0 1 Error on item: 2, Exception: This bit failed 3 4 Error on item: 5, Exception: This bit failed 6 7 8 9 

我希望这可以为将来的其他开发人员解决问题,因为一旦我们开始深入了解Linq和枚举,我们都会得到一个非常普遍的想法。 强大的东西,但有一些逻辑限制。

如果process对象一次只处理列表中的一个项目(如果可能的话),则可能更好,这样您就可以收集每个单独的exception并返回一个AggregateException ,就像Task Parallel库那样。

[如果流程在整个列表中运行,或者如果单个例外需要中止整个过程,那么这个建议显然不合适。]

您可以在工具箱中使用这些辅助函数,而不是仅解决您的特定问题:

  static IEnumerable RunEnumerator(Func> generator, Action onException) { using (var enumerator = generator()) { if (enumerator == null) yield break; for (; ; ) { //Avoid creating a default value with //unknown type T, as we don't know is it possible to do so T[] value = null; try { if (enumerator.MoveNext()) value = new T[] { enumerator.Current }; } catch (Exception e) { onException(e); } if (value != null) yield return value[0]; else yield break; } } } public static IEnumerable WithExceptionHandler(this IEnumerable orig, Action onException) { return RunEnumerator(() => { try { return orig.GetEnumerator(); } catch (Exception e) { onException(e); return null; } }, onException); } } 

WithExceptionHandlerIEnumerable转换为另一个,当发生exception时,将调用onException回调。 通过这种方式,您可以像这样实现您的processfunction:

 public override IEnumerable Process(IEnumerable incoming) { return incoming.WithExceptionHandler(e => { WriteToLog(e); throw; } } 

通过这种方式,您还可以执行其他操作,例如完全忽略exception并让迭代停止。 您也可以通过使用Func而不是Action来稍微调整它Func并允许exception回调来决定迭代是否继续。

不需要循环和良​​率(至少在您的示例中)。 简单地删除它们并且不再限制catch块。

 public override IEnumerable Process(IEnumerable incoming) { try { return this.processor.Process(incoming); } catch (Exception e) { WriteToLog(e); throw; } } 

我假设this.processor.Process(incoming)返回一个实现IEnumerable的集合,因此你不需要创建一个新的迭代器。 如果this.processor.Process(incoming)是惰性评估的话

 public override IEnumerable Process(IEnumerable incoming) { try { return this.processor.Process(incoming).ToList(); } catch (Exception e) { WriteToLog(e); throw; } }