在 .Net/C# 中抛出多个异常

42

我参与维护的一个应用程序中,任何业务逻辑错误都会导致异常被抛出,然后调用代码会处理这个异常。这种模式在整个应用程序中都被使用,并且运行良好。

现在我面临这样一种情况:我需要在业务层内尝试执行多个业务任务,其中一个任务的失败不应该导致整个流程终止,其他任务仍应能够执行。换句话说,这不是一个原子操作。我的问题是,在操作结束时,我希望通过抛出异常来通知调用代码发生了一个或多个异常。以下是伪代码片段:

function DoTasks(MyTask[] taskList)
{
  foreach(MyTask task in taskList)
  {
    try
    {
       DoTask(task);
    }
    catch(Exception ex)
    {
        log.add(ex);
    }
  }

  //I want to throw something here if any exception occurred
}

我该抛出什么?我在我的职业生涯中遇到过这种模式。以前我曾经列出所有异常的列表,然后抛出一个包含所有捕获异常的异常。但这似乎不是最优雅的方法。重要的是要尽可能地保留每个异常的详细信息以便呈现给调用代码。

你有什么想法?


编辑:解决方案必须使用 .Net 3.5 编写。我不能使用任何 beta 库,或者像 Bradley Grainger(下面)提到的 .Net 4.0 中的 AggregateException 来收集异常并进行抛出。


只需重新抛出异常。在记录日志(和执行其他操作)后,重新抛出它以使其沿着堆栈向上冒泡。 - Kon
1
哪个异常?我的情况假定有多个异常。 - Jason Jackson
我刚刚更新了我的回答,指出你可以在.NET 3.5上克隆AggregateException,这样当真正的.NET 4.0类型发布时,就可以轻松升级。你仍然需要编写代码(来克隆类),但它是向前兼容的。 - Bradley Grainger
Bradley,我看到了。我想采用类似的方法。我正在编写一个名为CollatedException的类,只是为了使它有一个不同的名称,但是模仿AggregateException。当4.0版本出来时,我可能会将其替换。 - Jason Jackson
@Jason Jackson:听起来这是一个不错的选择。如果最终的.NET 4.0类不合适/不能使用,你的代码可以保持不变。否则,将很容易进行重构以统一你的代码和最新的框架。 - Bradley Grainger
5个回答

45

.NET的任务并行库扩展(它将成为.NET 4.0的一部分)遵循其他答案建议的模式:将所有已抛出的异常收集到AggregateException类中。

通过始终抛出相同类型的异常(无论子工作是否有一个或多个异常),处理异常的调用代码更容易编写。

在.NET 4.0 CTP中,AggregateException具有公共构造函数(接受IEnumerable<Exception>),它可能是您的应用程序的不错选择。

如果你的目标是 .NET 3.5,可以考虑克隆你在自己代码中需要的 System.Threading.AggregateException 类的部分内容,例如一些构造函数和 InnerExceptions 属性。(你可以将你的克隆放置在你的程序集内的 System.Threading 命名空间中,如果你公开它可能会引起混淆,但以后升级到 4.0 更容易。)当 .NET 4.0 发布时,你应该能够通过从项目中删除包含克隆的源文件、更改项目以定位新的框架版本并重新构建来“升级”到 Framework 类型。当然,如果你这样做,需要仔细跟踪 Microsoft 发布的新 CTPs 对此类所做的更改,以避免你的代码变得不兼容。(例如,这似乎是一个有用的通用类,他们可以将其从 System.Threading 移动到 System。)在最坏的情况下,你可以只重命名类型并将其移回到你自己的命名空间中(这对大多数重构工具来说非常容易)。

我多么希望我们已经拥有这个库。我已经在旁边试用了一下,它真的很棒。不幸的是,我们的目标是 .Net 3.5。 - Jason Jackson
+1 我在寻找聚合异常,我知道这种类型存在,但是想不起来它的名字了。 - Chris Marisic
这是一个指向 MSDN 上 AggregateException 的参考链接:http://msdn.microsoft.com/zh-cn/library/system.aggregateexception.aspx。 - Corin
1
一个关于使用的MSDN杂志文章:http://msdn.microsoft.com/en-us/magazine/ee321571.aspx - Corin
显示剩余2条评论

14

我脑海中有两种方法,一种是创建自定义异常并将这些异常添加到该类中,最后抛出它:

public class TaskExceptionList : Exception
{
    public List<Exception> TaskExceptions { get; set; }
    public TaskExceptionList()
    {
        TaskExceptions = new List<Exception>();
    }
}

    public void DoTasks(MyTask[] taskList)
    {
        TaskExceptionList log = new TaskExceptionList();
        foreach (MyTask task in taskList)
        {
            try
            {
                DoTask(task);
            }
            catch (Exception ex)
            {
                log.TaskExceptions.Add(ex);
            }
        }

        if (log.TaskExceptions.Count > 0)
        {
            throw log;
        }
    }

如果任务失败并且有一个名为'out List'的变量,则返回true或false。

    public bool TryDoTasks(MyTask[] taskList, out List<Exception> exceptions)
    {
        exceptions = new List<Exception>();
        foreach (MyTask task in taskList)
        {
            try
            {
                DoTask(task);
            }
            catch (Exception ex)
            {
                exceptions.Add(ex);
            }
        }

        if (exceptions.Count > 0)
        {
            return false;
        }
        else
        {
            exceptions = null;
            return true;
        }
    }

1
我喜欢第二种解决方案,但是我正在处理的项目已经在所有地方内置了“对于业务规则违反抛出异常”的模式,这真的会违反这种模式。 - Jason Jackson

4
您可以创建一个自定义异常,其中包含一组异常。然后,在您的Catch块中,只需将其添加到该集合中。在您的过程结束时,检查异常计数是否大于0,然后抛出您的自定义异常。

1
这就是问题本身给出的答案。 - asterite

2
你可能需要使用BackgroundWorker来完成此操作。它会自动捕获和呈现任何完成时的异常,然后您可以将其抛出、记录或进行其他处理。此外,您还可以获得多线程的好处。
BackgroundWorker是一个很好的封装,它围绕委托的异步编程模型提供了支持。

1
这是一个好主意,但聚合和重新抛出的问题仍然存在。此外,我的伪代码是实际多线程场景的简化版,在那种情况下可以从后台线程中检索到多个异常。我只是不想用那些信息混淆问题。 - Jason Jackson

0

这里没有超级优雅的解决方案,但有一些想法:

  • 将错误处理程序函数作为参数传递给 DoTasks,以便用户可以决定是否继续
  • 使用跟踪记录错误发生的情况
  • 将其他异常的消息连接到异常包的消息中

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