Cet article a fait l'objet d'une traduction automatique.
.NET matters
Agrégation des exceptions
Stephen Toub
Les exceptions dans .NET sont le mécanisme fondamental par lequel les erreurs et les autres conditions exceptionnelles sont communiquées. Les exceptions sont utilisées non seulement pour stocker des informations sur ces problèmes, mais aussi pour propager ces informations dans formulaire instance d'objet via une pile d'appel. Basé sur la Windows gestion structurée des exceptions (SEH) modèle, qu'un seul .NET exception peut être «en vol"à tout moment donné sur un thread particulier, une restriction qui est généralement naire une pensée dans esprit des développeurs. Après tout, une seule opération donne généralement qu'une seule exception, et donc dans le code séquentiel nous écrire la plupart du temps, que nous devons préoccuper de la seule exception à la fois. Toutefois, il existe de nombreux scénarios dans lesquels plusieurs exceptions risque d'une opération. Cela inclut (mais est pas limité à) scénarios impliquant le parallélisme et de simultanéité.
Considérez le déclenchement d'un événement dans .NET :
public event EventHandler MyEvent;
protected void OnMyEvent() {
EventHandler handler = MyEvent;
if (handler != null) handler(this, EventArgs.Empty);
}
Plusieurs délégués peuvent être enregistrés avec MyEvent et lorsque le délégué de gestionnaire dans l'extrait de code précédent est appelé, l'opération est équivalente au code comme suit :
foreach(var d in handler.GetInvocationList()) {
((EventHandler)d)(this, EventArgs.Empty);
}
Chaque délégué constitue le délégué multicast gestionnaire est appelé une après l'autre. Toutefois, si une exception est levée à partir des appels des, la boucle foreach cesse traitement, ce qui signifie que des délégués ne peuvent pas être exécutées dans le cas d'une exception. Par exemple, prenons le code suivant :
MyEvent += (s, e) => Console.WriteLine("1");
MyEvent += (s, e) => Console.WriteLine("2");
MyEvent += (s, e) => { throw new Exception("uh oh"); };
MyEvent += (s, e) => Console.WriteLine("3");
MyEvent += (s, e) => Console.WriteLine("4");
Si MyEvent est appelée maintenant, «1» et «2» sont affichés dans la console, une exception est levée et les délégués ont affiche «3» et «4» ne puisse être appelé.
Pour vous assurer que tous les délégués sont appelés même en face d'une exception, il peut réécrire notre méthode OnMyEvent comme suit :
protected void OnMyEvent() {
EventHandler handler = MyEvent;
if (handler != null) {
foreach (var d in handler.GetInvocationList()) {
try {
((EventHandler)d)(this, EventArgs.Empty);
}
catch{}
}
}
}
Nous vous maintenant intercepter toutes les exceptions d'échappement d'un gestionnaire inscrit, autorisant des délégués qui viennent après une exception à être appelée. Si vous exécutez à nouveau l'exemple précédent, vous verrez que «1», «2», «3» et «4» est sortie, même si une exception est levée parmi les délégués. Malheureusement, cette nouvelle implémentation est manger également les exceptions, une pratique considérablement frowned sur. Ces exceptions peuvent indiquer un problème sérieux avec l'application, un problème qui n'est pas en cours traitée, car les exceptions sont ignorées.
Ce que nous voulons vraiment est de capturer les exceptions susceptibles de sortir et puis une fois que nous avons terminé appeler tous les gestionnaires d'événements, levez à nouveau toutes les exceptions qui séquence d'échappement à partir d'un gestionnaire. Bien sûr, comme nous l'avons déjà mentionné, instance d'une seule exception peut être levée sur un thread donné à un moment donné. Entrez AggregateException.
Dans le .NET Framework 4, la System.AggregateException est un nouveau type exception figurant dans mscorlib. En un type relativement simple, il permet une multitude de scénarios en fournissant des fonctionnalités exception centrale et utile.
AggregateException est lui-même une exception (dérivent de System.Exception) qui contient des autres exceptions. La classe System.Exception base a déjà la notion d'encapsuler une instance d'exception unique, appelée «exception interne». L'exception interne, exposée via la propriété InnerException lors d'une exception, représente la cause de l'exception et est souvent utilisée par les structures que fonctionnalités de couche et qui utilisent des exceptions pour élever les informations fournies. Par exemple, un composant qui traite les données d'entrée à partir d'un flux peut rencontrer une exception IOException lors de la lecture à partir du flux. Il peut ensuite créer une CustomParserException qui encapsule l'instance IOException en tant que InnerException, des détails de niveau supérieur sur la cause de l'opération analyse tout en fournissant la IOException pour le niveau inférieur et sous-jacent détails.
AggregateException étend simplement qui prennent en charge pour activer l'habillage des exceptions internes — au pluriel. Il fournit des constructeurs qui acceptent des tableaux params ou enumerables de ces exceptions internes (en plus au constructeur standard qui accepte une exception interne), et il expose les exceptions internes via une propriété InnerExceptions (en plus de la propriété InnerException de la classe de base). Voir la figure 1 pour une vue d'ensemble de surface publique de AggregateException.
La figure 1 System.AggregateException
[Serializable]
[DebuggerDisplay("Count = {InnerExceptions.Count}")]
public class AggregateException : Exception
{
public AggregateException();
public AggregateException(params Exception[] innerExceptions);
public AggregateException(IEnumerable<Exception> innerExceptions);
public AggregateException(string message);
public AggregateException(string message, Exception innerException);
public AggregateException(string message,
params Exception[] innerExceptions);
public AggregateException(string message,
IEnumerable<Exception> innerExceptions);
public AggregateException Flatten();
public void Handle(Func<Exception, bool> predicate);
public ReadOnlyCollection<Exception> InnerExceptions { get; }
}
Si la AggregateException ne possède pas les exceptions internes, InnerException renvoie null et InnerExceptions renvoie une collection vide. Si la AggregateException est fournie avec une seule exception à renvoyer à la ligne, retours InnerException (comme vous souhaitez vous y attendre) de l'instance et InnerExceptions renvoie une collection avec simplement qu'une exception. Si la AggregateException est fournie avec plusieurs exceptions pour encapsuler, InnerExceptions renvoie toutes les personnes dans la collection et InnerException retourne le premier élément de cette collection.
Maintenant, avec AggregateException, nous pouvez améliorer notre code de déclenchement d'événements .NET comme illustré à la figure 2 et nous sommes en mesure de disposer de notre gâteau et manger, trop. Les délégués inscrits auprès de l'événement continuent de fonctionner même si un lève une exception, et encore nous ne pas perdre les informations exceptionnelles car ils vous tout encapsulés dans un agrégat et levées à nouveau à la fin (uniquement si un des délégués échoue, bien sûr).
La figure 2 Utilisation AggregateException lorsque le déclenchement des événements
protected void OnMyEvent() {
EventHandler handler = MyEvent;
if (handler != null) {
List<Exception> exceptions = null;
foreach (var d in handler.GetInvocationList())
{
try {
((EventHandler)d)(this, EventArgs.Empty);
}
catch (Exception exc) {
if (exceptions == null)
exceptions = new List<Exception>();
exceptions.Add(exc);
}
}
if (exceptions != null) throw new AggregateException(exceptions);
}
}
Événements fournissent un exemple solide du où l'exception d'agrégation est utile pour code séquentiel. Cependant, AggregateException est d'importance prime pour le parallélisme nouvel constructions dans .NET 4 (et, en fait, même si AggregateException est utile pour le code non parallèle, le type a été créé et ajouté à .NET Framework par l'équipe Parallel Computing Platform de Microsoft).
Prendre en compte la nouvelle méthode Parallel.for de 4 .NET, qui est utilisé pour parallélisation une boucle for. Dans typique de boucle, une seule itération de cette boucle peut exécuter simultanément, ce qui signifie que seule exception peut se produire à la fois. Toutefois, avec un «boucle» parallèle, plusieurs itérations peuvent exécuter en parallèle et plusieurs itérations peuvent lever des exceptions simultanément. Un seul thread appelle la méthode Parallel.for, qui peut logiquement lever plusieurs exceptions, et nous avons donc un mécanisme par lequel ces plusieurs exceptions peuvent être propagées sur un seul thread d'exécution. Parallel.for gère cette par collecte les exceptions levées et leur propagation encapsulés dans une AggregateException. Le reste des méthodes de parallèle (Parallel.foreach et Parallel.Invoke) gérer les éléments de même, que Parallel LINQ (PLINQ), également partie 4 de .NET. Dans une requête LINQ-to-Objects, seul un utilisateur délégué est appelé à la fois, mais avec PLINQ, plusieurs utilisateurs délégués peuvent être appelées en parallèle, ces délégués peuvent lever des exceptions et PLINQ concerne que par les rassembler dans une AggregateException et propager ce regroupement.
Comme exemple de ce type de l'exécution en parallèle, considérez la figure 3 , qui présente une méthode qui utilise le pool de threads pour appeler plusieurs délégués action fourni par l'utilisateur en parallèle. (Une implémentation plus robuste et évolutive de cette fonctionnalité existe dans .NET 4 de la classe Parallel.) Le code utilise QueueUserWorkItem pour exécuter chaque action. Si le délégué action lève une exception, plutôt qu'autoriser cette exception se propager et accéder non gérée (qui, par défaut, les résultats dans le processus est détruite), le code capture l'exception et la stocke dans une liste partagée par tous les éléments de travail. Une fois tous les appels asynchrones ont terminé (correctement ou exceptionnellement), une AggregateException est levée avec les exceptions capturées, si les ont été capturées. (Notez que ce code peut être utilisé dans OnMyEvent pour exécuter tous les délégués enregistrés avec un événement en parallèle).
La figure 3 AggregateException dans appel parallèle
public static void ParallelInvoke(params Action[] actions)
{
if (actions == null) throw new ArgumentNullException("actions");
if (actions.Any(a => a == null)) throw new ArgumentException ("actions");
if (actions.Length == 0) return;
using (ManualResetEvent mre = new ManualResetEvent(false)) {
int remaining = actions.Length;
var exceptions = new List<Exception>();
foreach (var action in actions) {
ThreadPool.QueueUserWorkItem(state => {
try {
((Action)state)();
}
catch (Exception exc) {
lock (exceptions) exceptions.Add(exc);
}
finally {
if (Interlocked.Decrement(ref remaining) == 0) mre.Set();
}
}, action);
}
mre.WaitOne();
if (exceptions.Count > 0) throw new AggregateException(exceptions);
}
}
L'espace de noms System.Threading.Tasks nouvelle dans .NET 4 également utilise libéral AggregateExceptions. Une tâche dans .NET 4 est un objet qui représente une opération asynchrone. Contrairement à QueueUserWorkItem, qui ne fournit un mécanisme pour désigner le travail en file d'attente, tâches fournit un handle pour le travail asynchrone, l'activation d'un grand nombre d'opérations importantes à effectuer, comme en attente d'un élément de travail terminer ou continuer de celui-ci pour effectuer une opération lorsque le travail se termine. Les méthodes parallèle mentionnés reposent sur tâches, que PLINQ.
Alors tout l'explication de AggregateException, une construction facile à raison sur ici est la méthode Task.WaitAll statique. Vous transmettez à WaitAll les instances de tâche à attendre et WaitAll «blocs» tant que ces instances de tâches sont terminées. (J'ai placé entre guillemets «blocs», car la méthode WaitAll peut aider réellement à exécuter les tâches de façon à réduire la consommation de ressources et de fournir une meilleure efficacité que tout bloquer un thread.) Si les tâches de tous les terminée, le code placé sur sa manière Joyeux. Toutefois, plusieurs tâches peuvent ont des exceptions levées et WaitAll peut propager uniquement une exception à son thread d'appel, qui encapsule les exceptions dans une seule AggregateException et lève cette agrégation.
Les tâches utilisent AggregateExceptions dans d'autres endroits ainsi. Un ne soit pas aussi évident est relations parent/enfant entre les tâches. Par défaut, les tâches créés pendant l'exécution d'une tâche sont apparentés à cette tâche, fournissant un formulaire de parallélisme structuré. Par exemple, tâche crée la tâche B et C de tâche et cela tâche est considérée comme le parent de tâche B et c. de tâches Ces relations entrent en jeu principalement en tenir compte de durée de vie. Une tâche n'est pas considéré comme terminé tant que tous ses enfants sont terminées, afin que si vous utilisiez attendre sur tâche, cette instance d'attente n'est pas retourner jusqu'à ce que B et C avaient également terminée. Ces relations parent/enfant affectent non seulement l'exécution dans ce tenir compte, mais elles sont également visibles via les nouvelles fenêtres Outil débogueur Visual Studio 2010, ce qui simplifie le débogage de certains types de charges de travail considérablement.
Prenons le code comme suit :
var a = Task.Factory.StartNew(() => {
var b = Task.Factory.StartNew(() => {
throw new Exception("uh");
});
var c = Task.Factory.StartNew(() => {
throw new Exception("oh");
});
});
...
a.Wait();
Ici, tâche a deux enfants, qui elle implicitement attend avant est considérée comme terminée, et deux de ces enfants lèvent des exceptions non gérées. Pour tenir compte de cela, tâche encapsule les exceptions de ses enfants dans une AggregateException et c'est ce regroupement est renvoyé à partir de l'exception propriété et levée hors d'un appel à Wait sur a.
Comme j'ai démontré, AggregateException peut être un outil très utile. Pour raisons de cohérence et facilité d'utilisation, cependant, il peut également entraîner conceptions peuvent tout d'abord être paradoxale. Pour clarifier ce que signifient, prenons la fonction suivante :
public void DoStuff()
{
var inputNum = Int32.Parse(Console.ReadLine());
Parallel.For(0, 4, i=> {
if (i < inputNum) throw new MySpecialException(i.ToString());
});
}
Ici, en fonction de l'utilisateur, le code contenu dans la boucle parallèle peut lever 0, 1 ou plusieurs exceptions. Prenons maintenant le code que vous devrez écrire gérer ces exceptions. Si Parallel.for encapsulé exceptions dans une AggregateException uniquement lorsque plusieurs exceptions ont été levées, vous, comme le consommateur de DoStuff, devez écrire deux distinct catch gestionnaires : un pour le cas dans lesquels seule MySpecialException s'est produite et un pour le cas dans lesquels une AggregateException s'est produite. Le code pour traiter la AggregateException serait probablement rechercher InnerExceptions de AggregateException une MySpecialException, puis exécutez le même code de gestion pour cette exception individuelle qui vous souhaitez avoir dans le bloc catch dédié à MySpecialException. À mesure que vous commencez à traiter avec plus d'exceptions, ce problème de duplication augmente. Pour résoudre ce problème ainsi que pour fournir la cohérence, méthodes de 4 .NET telles que Parallel.for devant traiter les risques de plusieurs exceptions toujours encapsulez, même si une seule exception se produit. De cette façon, vous devez écrire uniquement un bloc catch pour AggregateException. L'exception à cette règle est que les exceptions qui ne peuvent jamais se produire dans une portée simultanée seront encapsulées pas. Ainsi, par exemple, les exceptions qui peuvent entraîner de Parallel.for en raison de validation de ses arguments et recherche un d'eux pour être null ne seront pas encapsulées. Cette validation d'argument survient avant que Parallel.for tourne désactiver n'importe quel travail asynchrone, et par conséquent il est impossible que plusieurs exceptions peuvent se produire.
Bien sûr, des exceptions encapsulées dans une AggregateException peuvent également entraîner des difficultés dans que vous avez maintenant deux modèles pour traiter : dévoilé et encapsuler des exceptions. Pour faciliter la transition entre les deux, AggregateException fournit plusieurs méthodes d'assistance pour faciliter l'utilisation de ces modèles.
La première méthode d'assistance est écraser. Comme j'ai mentionné, AggregateException est lui-même une exception, afin qu'il peut être levée. Toutefois, cela signifie que AggregateException instances peuvent envelopper des autres instances AggregateException, et, en fait, c'est une occurrence probable, en particulier lorsque des fonctions récursives peuvent lever des agrégats. Par défaut, AggregateExceptions conserve cette structure hiérarchique, ce qui peut être utile lors du débogage, car la structure hiérarchique des agrégats contenus sera probablement correspond à la structure du code qui a généré ces exceptions. Toutefois, cela peut également compliquer agrégats à utiliser dans certains cas. Pour tenir compte pour ce faire, la méthode Flatten supprime les couches d'agrégats contenues en créant une nouveau AggregateException qui contient les AggregateExceptions non de la hiérarchie entière. Par exemple, supposons que j'ai eu la structure suivante des instances d'exception :
- AggregateException
- InvalidOperationException
- ArgumentOutOfRangeException
- AggregateException
- IOException
- DivideByZeroException
- AggregateException
- FormatException
- AggregateException
- TimeZoneException
Si vous appelez écraser sur l'instance AggregateException externe, j'obtiens une nouveau AggregateException avec la structure suivante :
- AggregateException
- InvalidOperationException
- ArgumentOutOfRangeException
- IOException
- DivideByZeroException
- FormatException
- TimeZoneException
Il est ainsi beaucoup plus facile pour moi en boucle et examiner InnerExceptions de l'agrégat, sans vous préoccuper des parcours de manière récursive contenus agrégats.
La deuxième méthode d'assistance, handle, facilite ce parcours. Handle possède la signature suivante :
public void Handle(Func<Exception,bool> predicate);
Voici une approximation de son implémentation :
public void Handle(Func<Exception,bool> predicate)
{
if (predicate == null) throw new ArgumentNullException("predicate");
List<Exception> remaining = null;
foreach(var exception in InnerExceptions) {
if (!predicate(exception)) {
if (remaining == null) remaining = new List<Exception>();
remaining.Add(exception);
}
}
if (remaining != null) throw new AggregateException(remaining);
}
Handle parcourt InnerExceptions dans la AggregateException et évalue une fonction de prédicat pour chacun d'eux. Si la fonction de prédicat renvoie la valeur true pour une instance d'exception donné, qu'exception est considérée comme gérée. Si, toutefois, le prédicat retourne false, cette exception est levée de traiter à nouveau cadre d'une nouveau AggregateException contenant toutes les exceptions impossibles à faire correspondre le prédicat. Cette approche peut être utilisée pour filtrer rapidement les exceptions que vous importe peu sur ; par exemple :
try {
MyOperation();
}
catch(AggregateException ae) {
ae.Handle(e => e is FormatException);
}
Qui appeler pour filtre traiter n'importe quel FormatExceptions à partir de la AggregateException est interceptée. Si des exceptions outre FormatExceptions, uniquement les exceptions sont levées à nouveau en tant qu'une partie de la nouvelle AggregateException, et si il ne sont pas toutes les exceptions non FormatException, handle renvoie avec succès avec rien de nouveau levée. Dans certains cas, il peut également s'avérer utile pour tout d'abord Aplatir les fonctions d'agrégation, comme vous le voyez ici :
ae.Flatten().Handle(e => e is FormatException);
Bien sûr, à la base une AggregateException est simplement un conteneur pour les autres exceptions, et vous pouvez écrire vos propres méthodes d'assistance pour travailler avec ces exceptions contenues d'une manière conforme aux exigences de votre application. Par exemple, peut-être vous intéressent plus simplement lever une exception à conserver toutes les exceptions. Vous pouvez écrire une méthode d'extension comme suit :
public static void PropagateOne(this AggregateException aggregate)
{
if (aggregate == null) throw new ArgumentNullException("aggregate");
if (aggregate.InnerException != null)
throw aggregate.InnerException; // just throw one
}
que vous pouvez ensuite utiliser comme suit :
catch(AggregateException ae) { ae.PropagateOne(); }
Ou peut-être que vous souhaitez filtrer pour afficher uniquement les exceptions qui correspondent à certains critères et puis d'agrégation d'informations sur ces exceptions. Par exemple, vous pouvez avoir une AggregateException contenant une série entière de ArgumentExceptions et vous voulez synthétiser les paramètres a provoqué des problèmes :
AggregateException aggregate = ...;
string [] problemParameters =
(from exc in aggregate.InnerExceptions
let argExc = exc as ArgumentException
where argExc != null && argExc.ParamName != null
select argExc.ParamName).ToArray();
Tout, System.AggregateException nouveau est un outil simple mais puissant, particulièrement pour les applications ne peut pas vous permettre de vous permettent de n'importe quelle exception passer inaperçus. Pour le débogage, ToString implémentation de AggregateException génère une chaîne de rendu de toutes les exceptions qu'il contient. Et comme vous pouvez le voir dans la figure 1 , même un DebuggerDisplayAttribute AggregateException vous aider à rapidement identifier combien exceptions contient une AggregateException.
Envoyez vos questions et commentaires à Stephen à netqa@microsoft.com.
Stephen Toub est un responsable de programme senior dans l'équipe Parallel Computing Platform de Microsoft. Il est également un éditeur contribuant pour MSDN Magazine.