捕获AggregateException

28

我试图抛出和捕获一个AggregateException异常。 在C#中,我并没有经常使用异常,但我发现的行为有点令人惊讶。

我的代码如下:

var numbers = Enumerable.Range(0, 20);

try
{
    var parallelResult = numbers.AsParallel()
        .Where(i => IsEven(i));
    parallelResult.ForAll(e => Console.WriteLine(e));

}
catch (AggregateException e)
{
    Console.WriteLine("There was {0} exceptions", e.InnerExceptions.Count());
}

正在调用函数IsEven

private static bool IsEven(int i)
{
    if (i % 10 == 0)
        throw new AggregateException("i");
    return i % 2 == 0;
}

那会抛出 AggregateException 异常。

我期望代码在 0-20 范围内写入每个偶数和两次 "There was 1 exceptions"。

实际上,我得到了一些数字的输出(由于 ForAll 的随机性质),然后抛出了异常,但是没有被捕获,程序停止了。

我错过了什么吗?


2
我不确定为什么会发生这种情况,尝试将 throw new AggregateException("i"); 更改为 throw new ArgumentException("i"); 可以产生预期的结果。 - Sriram Sakthivel
6
你现在把系统搞混了,因为你抛出了错误的异常。应该抛出"Argument"或者"InvalidOperationException"异常。 - H H
1
@SriramSakthivel - 为什么你的版本会导致应用程序崩溃 - 我只能猜测,但它的InnerExceptions将为空。出乎意料。 - H H
1
@SriramSakthivel - 但内部代码不应该抛出Aggregate异常。这不是它们的职责。 - H H
1
@HenkHolterman 当然,我知道这是一个不好的想法,AggregateException的目的是将许多异常组合起来并保留StackTrace。但我找不到任何文件说明我们不应该这样做。 - Sriram Sakthivel
显示剩余5条评论
2个回答

26

这实际上很有趣。我认为问题在于您以意外的方式使用了AggregateException,这会导致PLINQ代码出错。

AggregateException的整个重点是将可能同时(或几乎同时)发生的多个异常分组在一起。因此,AggregateException应该至少有一个内部异常。但是您却抛出了new AggregateException("i"),它没有内部异常。PLINQ代码试图检查InnerExceptions属性,遇到某种错误(可能是NullPointerException),然后似乎进入了某种循环。尽管这是一个不寻常的构造函数,但这显然是PLINQ中的一个错误。

正如其他地方指出的那样,抛出ArgumentException会更具语义正确性。但是,您可以通过抛出正确构造的AggregateException来获得您所需的行为,例如将IsEven函数更改为以下内容:

private static bool IsEven(int i)
{
    if (i % 10 == 0){
        //This is still weird
        //You shouldn't do this. Just throw the ArgumentException.
        throw new AggregateException(new ArgumentException("I hate multiples of 10"));
    }
    return i % 2 == 0;
}

我认为这个故事的道德是,除非你确切地知道自己在做什么,特别是如果你已经在并行或基于Task的操作中,否则不要抛出AggregateException


请编辑代码,确保使用ArgumentException而不是AggregateException。虽然您最后提到了这一点,但我担心只看一眼的人会认为发布的代码是正确的(实际上并不是)。 - X.J
1
加了一条注释,说你不应该像我这样做。 - Brian Reischl

8
我同意其他人的看法:这是.NET中的一个bug,您应该报告它
原因在于内部类QueryTaskGroupState中的QueryEnd()方法。 它的反编译代码(为了清晰起见略作修改)如下:
try
{
  this.m_rootTask.Wait();
}
catch (AggregateException ex)
{
  AggregateException aggregateException = ex.Flatten();
  bool cacellation = true;
  for (int i = 0; i < aggregateException.InnerExceptions.Count; ++i)
  {
    var canceledException =
        aggregateException.InnerExceptions[i] as OperationCanceledException;
    if (IsCancellation(canceledException))
    {
      cacellation = false;
      break;
    }
  }
  if (!cacellation)
    throw aggregateException;
}
finally
{
  this.m_rootTask.Dispose();
}
if (!this.m_cancellationState.MergedCancellationToken.IsCancellationRequested)
  return;
if (!this.m_cancellationState.TopLevelDisposedFlag.Value)
  CancellationState.ThrowWithStandardMessageIfCanceled(
    this.m_cancellationState.ExternalCancellationToken);
if (!userInitiatedDispose)
  throw new ObjectDisposedException(
    "enumerator", "The query enumerator has been disposed.");

基本上,这个代码做的事情是:
  • 如果抛出的AggregateException包含任何非取消异常,则重新抛出该异常。
  • 如果请求取消,则抛出新的取消异常(或者直接返回而不抛出异常,我不太理解这一部分,但我认为在这里不相关)。
  • 否则会因某种原因抛出ObjectDisposedException(假设userInitiatedDisposefalse,确实是这样)。
所以,如果你抛出一个没有内部异常的AggregateException,那么ex将是一个包含空AggregateExcaptionAggregateException。调用Flatten()会将其转换为一个空的AggreateException,这意味着它不包含任何非取消异常,因此代码的第一部分认为这是取消操作,并且不抛出异常。
但是,代码的第二部分意识到这不是取消操作,因此它会抛出一个完全虚假的异常。

3
8年后,这个漏洞在.NET 5中仍然存在。 - SerG
2
报告:https://github.com/dotnet/core/issues/5900 - SerG
1
根据链接所示,问题已在.Net 6中修复,只是想让大家知道。 - undefined

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