web-dev-qa-db-fra.com

Attraper AggregateException

J'essaie de lancer et d'attraper une AggregateException. Je n'ai pas beaucoup utilisé d'exceptions en C #, mais le comportement que j'ai trouvé est un peu surprenant.

Mon code est:

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());
}

Il appelle la fonction IsEven

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

Cela lève l'exception AggregateException.

Je m'attendrais à ce que le code écrive chaque nombre pair dans la plage 0,20 et "Il y avait 1 exceptions" deux fois.

Ce que j'obtiens, c'est quelques chiffres imprimés (ils sont une cause aléatoire de ForAll), puis l'exception est levée, mais pas interceptée et les programmes s'arrêtent.

Suis-je en train de manquer quelque chose?

26
MaPi

C'est en fait assez intéressant. Je pense que le problème est que vous utilisez AggregateException d'une manière inattendue, ce qui provoque une erreur dans le code PLINQ.

L'intérêt de AggregateException est de regrouper plusieurs exceptions qui peuvent se produire simultanément (ou presque) dans un processus parallèle. Ainsi, AggregateException devrait avoir au moins une exception interne. Mais vous lancez new AggregateException("i"), qui n'a pas d'exceptions internes. Le code PLINQ essaie d'examiner la propriété InnerExceptions, frappe une sorte d'erreur (probablement une NullPointerException), puis il semble entrer dans une boucle quelconque. Il s'agit sans doute d'un bogue dans PLINQ, car vous utilisez un constructeur valide pour AggregateException, même s'il est inhabituel.

Comme indiqué ailleurs, lancer ArgumentException serait plus sémantiquement correct. Mais vous pouvez obtenir le comportement que vous recherchez en lançant un AggregateException correctement construit, par exemple en changeant la fonction IsEven en quelque chose comme ceci:

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;
}

Je pense que la morale de l'histoire est de ne pas lancer AggregateException à moins que vous ne sachiez vraiment exactement ce que vous faites, en particulier si vous êtes déjà dans une opération parallèle ou basée sur Task .

22
Brian Reischl

Je suis d'accord avec les autres: c'est un bogue dans .Net et vous devriez le signaler .

La cause est dans la méthode QueryEnd() dans la classe interne QueryTaskGroupState. Son code décompilé (et légèrement modifié pour plus de clarté) ressemble à ceci:

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.");

Fondamentalement, cela signifie:

  • renvoyer le AggregateException aplati s'il contient des exceptions de non-annulation
  • lancer une nouvelle exception d'annulation si l'annulation a été demandée (ou revenir sans lancer, je ne comprends pas vraiment cette partie, mais je ne pense pas que ce soit pertinent ici)
  • sinon lancer ObjectDisposedException pour une raison quelconque (en supposant que userInitiatedDispose est false, ce qui est le cas)

Donc, si vous lancez un AggregateException sans exception interne, ex sera un AggregateException contenant votre AggregateExcaption vide. L'appel de Flatten() transformera cela en un simple AggreateException, ce qui signifie qu'il ne contient aucune exception de non-annulation, donc la première partie du code pense que c'est une annulation et non jeter.

Mais la deuxième partie du code se rend compte que ce n'est pas une annulation, donc il lève une exception complètement fausse.

7
svick