Exceptions

Références

L'actualité

Librairie

L'information

Introduction

Les fonctionnalités de gestion des exceptions du langage C# vous aident à gérer les situations inattendues ou exceptionnelles qui se produisent lorsqu'un programme est en cours d'exécution. La gestion des exceptions utilise les mots clés try, catch et finally pour tenter des actions susceptibles de ne pas réussir, pour gérer les défaillances lorsque vous pensez que c'est justifié et pour nettoyer ensuite les ressources. Les exceptions peuvent être générées par le common language runtime (CLR), par .NET Framework ou des bibliothèques tierces, ou par le code de l'application. Les exceptions sont créées avec le mot clé throw.

Dans de nombreux cas, une exception peut être levée non pas par une méthode appelée directement par votre code, mais par une autre méthode plus loin dans la pile des appels. Dans ce cas, le CLR déroule la pile à la recherche d'une méthode avec un bloc catch pour le type d'exception concerné et exécute le premier bloc catch de ce type qu'il trouve. S'il ne trouve pas de bloc catch dans la pile des appels, il termine le processus et affiche un message à l'utilisateur.

Dans cet exemple, une méthode teste la division par zéro et intercepte l'erreur. Sans la gestion des exceptions, ce programme se terminerait avec une erreur DivideByZeroException non gérée.


Vue d'ensemble des exceptions

Les exceptions ont les propriétés suivantes :

Les exceptions sont des types qui dérivent tous en définitive de System.Exception.

Utilisez un bloc try autour des instructions qui peuvent lever des exceptions.

Dès qu'une exception se produit dans le bloc try, le flux de contrôle passe immédiatement au premier gestionnaire d'exceptions associé présent dans la pile des appels. En C#, le mot clé catch est utilisé pour définir un gestionnaire d'exceptions.

Si aucun gestionnaire d'exceptions n'est présent pour une exception donnée, le programme s'arrête avec un message d'erreur.

N'interceptez pas d'exception si vous ne pouvez pas la gérer tout en laissant l'application dans un état connu. Si vous interceptez System.Exception, levez-la de nouveau avec le mot clé throw à la fin du bloc catch.

Si un bloc catch définit une variable d'exception, vous pouvez l'utiliser pour obtenir plus d'informations sur le type d'exception qui s'est produit.

Les exceptions peuvent être générées explicitement par un programme avec le mot clé throw.

Les objets Exception contiennent des informations détaillées sur l'erreur, telles que l'état de la pile des appels et une description du texte de l'erreur.

Le code qui se trouve dans un bloc finally est exécuté même si une exception est levée. Utilisez un bloc finally pour libérer des ressources, par exemple pour fermer tous les flux ou fichiers qui ont été ouverts dans le bloc try.

Les exceptions gérées dans .NET Framework sont implémentées au-dessus du mécanisme de gestion structurée des exceptions de Win32. Pour plus d'informations, consultez les pages Gestion structurée des exceptions (C/C++) et Cours intensif sur la gestion structurée des exceptions de Win32.


Utilisation d'exceptions

En C#, les erreurs qui se produisent dans le programme au moment de l'exécution sont propagées à travers le programme à l'aide du mécanisme des exceptions. Les exceptions sont levées par le code qui rencontre une erreur et interceptées par le code qui peut corriger l'erreur. Les exceptions peuvent être levées par le CLR (Common Language Runtime) du .NET Framework ou par du code dans un programme. Une fois qu'une exception est levée, elle se propage jusqu'en haut de la pile des appels, jusqu'à ce qu'une instruction catch soit trouvée pour l'exception. Les exceptions non interceptées sont gérées par un gestionnaire d'exceptions génériques fourni par le système qui affiche une boîte de dialogue.

Les exceptions sont représentées par des classes dérivées de Exception. Cette classe identifie le type d'exception et contient des propriétés comportant des détails sur l'exception. Lever une exception implique de créer une instance d'une classe dérivée d'une exception, de configurer éventuellement les propriétés de l'exception, puis de lever l'objet à l'aide du mot clé throw. Par exemple :

Après la levée d'une exception, le runtime vérifie l'instruction actuelle pour voir si elle se trouve dans un bloc try. Si tel est le cas, tous les blocs catch associés au bloc try font l'objet d'un contrôle permettant de déterminer s'ils peuvent intercepter l'exception. Les blocs Catch spécifient généralement des types d'exception ; si le type du bloc catch est identique à celui de l'exception ou d'une classe de base de l'exception, le bloc catch peut gérer la méthode, par exemple :

Si l'instruction qui lève une exception ne se trouve pas à l'intérieur d'un bloc try ou si le bloc try qui la contient n'a pas de bloc catch correspondant, le runtime vérifie que la méthode d'appel dispose d'une instruction try et de blocs catch. Le runtime continue jusqu'à la pile appelante, à la recherche d'un bloc catch compatible. Une fois le bloc catch trouvé et exécuté, le contrôle est passé à l'instruction suivante après le bloc catch.

Une instruction try peut contenir plusieurs blocs catch. La première instruction catch qui peut gérer l'exception est exécutée ; toutes les instructions catch suivantes, même compatibles, sont ignorées. Par conséquent, les blocs catch doivent toujours être classés du plus spécifique (ou du plus dérivé) au moins spécifique, par exemple :

Avant que le bloc catch ne soit exécuté, le runtime vérifie la présence de blocs finally. Les blocs Finally permettent au programmeur de nettoyer tout état ambigu qui pourrait subsister après l'abandon d'un bloc try, ou de libérer les ressources externes (telles que les handles graphiques, les connexions de bases de données ou les flux de fichiers) sans attendre que le garbage collector du runtime ne finalise les objets, par exemple :

Si WriteByte() a levé une exception, le code du deuxième bloc try qui essaie de rouvrir le fichier échoue si file.Close() n'est pas appelé, et le fichier reste verrouillé. étant donné que les blocs finally sont exécutés même si une exception est levée, le bloc finally de l'exemple précédent autorise la fermeture correcte du fichier et permet d'éviter une erreur.

Si aucun bloc catch compatible n'est trouvé sur la pile des appels après la levée d'une exception, l'une des trois situations suivantes se produit :

  • Si l'exception se trouve dans un finaliseur, le finaliseur est abandonné et le finaliseur de base, le cas échéant, est appelé.
  • Si la pile des appels contient un constructeur statique ou un initialiseur de champ statique, une TypeInitializationException est levée, avec l'exception d'origine assignée à la propriété InnerException de la nouvelle exception.
  • Si le début du thread est atteint, le thread est terminé.

Gestion des exceptions

Un bloc try est utilisé par les programmeurs C# pour partitionner du code susceptible d'être affecté par une exception. Des blocs catch associés sont utilisés pour gérer les exceptions générées. Un bloc finally contient du code qui s'exécute dans tous les cas, qu'une exception soit levée ou non dans le bloc try (il peut s'agir par exemple de la libération des ressources allouées dans le bloc try). Un bloc try doit être associé à un ou plusieurs blocs catch, à un bloc finally, ou aux deux.

Les exemples suivants montrent une instruction try-catch, une instruction try-finally et une instruction try-catch-finally.



Un bloc try sans bloc catch ou finally provoque une erreur du compilateur.

Blocs catch

Un bloc catch peut spécifier le type d'exception à intercepter. La spécification de type est appelée filtre d'exception. Le type d'exception doit être dérivé de Exception. En général, ne spécifiez pas Exception comme filtre d'exception, sauf si vous savez comment gérer toutes les exceptions susceptibles d'être levées dans le bloc try ou si vous avez inclus une instruction throw à la fin de votre bloc catch.

Vous pouvez chaîner plusieurs blocs catch avec des filtres d'exception différents. Les blocs catch sont évalués de haut en bas dans votre code, mais un seul bloc catch est exécuté pour chaque exception levée. Le premier bloc catch qui spécifie le type exact ou une classe de base de l'exception levée est exécuté. Si aucun bloc catch ne spécifie un filtre d'exception correspondant, un bloc catch qui n'a pas de filtre est sélectionné, s'il y en a un dans l'instruction. Lors du positionnement des blocs catch, il est important de placer en premier les types d'exception les plus spécifiques (c'est-à-dire les plus dérivés).

Vous devez intercepter les exceptions quand les conditions suivantes sont remplies :

Vous savez pourquoi l'exception a été levée et vous pouvez implémenter une récupération spécifique, par exemple inviter l'utilisateur à entrer un nouveau nom de fichier quand vous interceptez un objet FileNotFoundException.

Vous pouvez créer et lever une nouvelle exception plus spécifique.

Vous voulez traiter partiellement une exception avant de la transmettre en vue d'un traitement supplémentaire. Dans l'exemple suivant, un bloc catch est utilisé pour ajouter une entrée à un journal d'erreurs avant de relever l'exception.


Blocs Finally

Un bloc finally vous permet de nettoyer les actions qui sont exécutées dans un bloc try. S'il est présent, le bloc finally s'exécute en dernier, après le bloc try et tout bloc catch mis en correspondance. Un bloc finally s'exécute toujours, qu'une exception soit levée ou non, et même si aucun bloc catch correspondant au type d'exception n'est trouvé.

Le bloc finally peut être utilisé pour libérer des ressources telles que des flux de fichiers, des connexions de base de données et des handles graphiques, sans attendre que le récupérateur de mémoire dans le runtime finalise les objets. Pour plus d'informations, consultez Instruction using.

Dans l'exemple suivant, le bloc finally est utilisé pour fermer un fichier ouvert dans le bloc try. Notez que l'état du handle de fichier est vérifié avant la fermeture du fichier. Si le bloc try ne peut pas ouvrir le fichier, le handle de fichier a encore la valeur null et le bloc finally n'essaie pas de le fermer. En guise d'alternative, si le fichier est ouvert avec succès dans le bloc try, le bloc finally ferme le fichier ouvert.


Création et levée d'exceptions

Les exceptions sont utilisées pour indiquer qu'une erreur s'est produite pendant l'exécution du programme. Les objets d'exception qui décrivent une erreur sont créés, puis levés avec le mot clé throw. Le runtime recherche ensuite le gestionnaire d'exceptions le plus compatible.

Les programmeurs doivent lever des exceptions quand une ou plusieurs des conditions suivantes sont vérifiées :

La méthode ne peut pas remplir sa fonction définie.

Par exemple, si un paramètre d'une méthode a une valeur non valide :

Un appel inapproprié à un objet est effectué en fonction de l'état de l'objet.

Une tentative d'écriture dans un fichier en lecture seule en est un exemple. Dans les cas où l'état d'un objet n'autorise pas une opération, levez une instance d'InvalidOperationException ou un objet basé sur une dérivation de cette classe. Voici un exemple d'une méthode qui lève un objet InvalidOperationException :

Quand un argument d'une méthode provoque une exception.

Dans ce cas, l'exception d'origine doit être interceptée et une instance d'ArgumentException doit être créée. L'exception d'origine doit être passée au constructeur d'ArgumentException comme paramètre InnerException :

Les exceptions contiennent une propriété nommée StackTrace. Cette chaîne contient le nom des méthodes sur la pile des appels actuelle, avec le nom de fichier et le numéro de ligne où l'exception a été levée pour chaque méthode. Un objet StackTrace est créé automatiquement par le CLR (Common Language Runtime) à partir du point de l'instruction throw, de sorte que les exceptions doivent être levées à partir du point où la trace de la pile doit commencer.

Toutes les exceptions contiennent une propriété nommée Message. Cette chaîne doit être définie pour expliquer la raison de l'exception. Notez que les informations sensibles du point de vue de la sécurité ne doivent pas être placées dans le texte du message. Outre Message, ArgumentException contient une propriété nommée ParamName dont la valeur doit être le nom de l'argument qui a provoqué la levée de l'exception. Dans le cas d'une méthode setter de propriété, ParamName doit être défini sur value.

Les méthodes publiques et protégées doivent lever des exceptions chaque fois qu'elles ne parviennent pas à remplir leurs fonctions habituelles. La classe d'exception qui est levée doit être l'exception la plus spécifique disponible qui répond aux conditions d'erreur. Ces exceptions doivent être documentées dans le cadre de la fonctionnalité de la classe. De plus, les classes dérivées ou les mises à jour de la classe d'origine doivent conserver le même comportement afin d'assurer la compatibilité descendante.

Pratiques à éviter lors de la levée d'exceptions

La liste suivante identifie les pratiques à éviter lors de la levée d'exceptions :

  • Les exceptions ne doivent pas être utilisées pour changer le flux d'un programme dans le cadre d'une exécution ordinaire. Elles doivent être utilisées uniquement pour signaler et gérer les conditions d'erreur.
  • Les exceptions ne doivent pas être retournées comme valeur de retour ou paramètre au lieu d'être levées.
  • Ne levez pas intentionnellement System.Exception, System.SystemException, System.NullReferenceException ni System.IndexOutOfRangeException à partir de votre propre code source.
  • Ne créez pas d'exceptions qui peuvent être levées en mode Debug mais pas en mode Release. Pour identifier des erreurs d'exécution pendant la phase de développement, utilisez plutôt Debug Assert.


Définition de classes d'exceptions

Les programmes peuvent lever une classe d'exceptions prédéfinie dans l'espace de noms System (sauf dans les endroits préalablement signalés) ou créer leurs propres classes d'exceptions en les dérivant d'Exception. Les classes dérivées doivent définir au moins trois constructeurs : un constructeur par défaut, un qui définit la propriété du message et un qui définit à la fois la propriété Message et la propriété InnerException. Le quatrième constructeur est utilisé pour sérialiser l'exception. Les nouvelles classes d'exception doivent être sérialisables. Par exemple :

Les nouvelles propriétés doivent être ajoutées à la classe d'exceptions uniquement quand les données qu'elles fournissent sont utiles à la résolution de l'exception. Si de nouvelles propriétés sont ajoutées à la classe d'exceptions dérivée, la méthode ToString() doit être substituée pour retourner les informations ajoutées.


Exceptions générées par le compilateur

Certaines exceptions sont levées automatiquement par le Common Language Runtime (CLR) du .NET Framework en cas d'échec d'opérations de base. Ces exceptions et leurs conditions d'erreur sont répertoriées dans le tableau suivant.

Exception Description
ArithmeticException Classe de base pour les exceptions qui se produisent pendant des opérations arithmétiques, telles que DivideByZeroException et OverflowException.
ArrayTypeMismatchException Levée quand un tableau ne peut pas stocker un élément donné, car le type réel de l'élément est incompatible avec le type réel du tableau.
DivideByZeroException Levée lors d'une tentative de division d'une valeur intégrale par zéro.
IndexOutOfRangeException Levée lors d'une tentative d'indexation d'un tableau à l'aide d'un index qui est inférieur à zéro ou en dehors des limites du tableau.
InvalidCastException Levée quand une conversion explicite d'un type de base en interface ou en un type dérivé échoue au moment de l'exécution.
NullReferenceException Levée quand vous essayez de référencer un objet dont la valeur est null.
OutOfMemoryException Levée quand une tentative d'allocation de mémoire à l'aide de l'opérateur new échoue. Cela indique que la mémoire disponible pour le Commun Language Runtime est épuisée.
OverflowException Levée quand une opération dans un contexte checked engendre un dépassement.
StackOverflowException Levée quand la pile d'exécution est épuisée par un trop grand nombre d'appels de méthode en attente ; cela indique généralement une récurrence très profonde ou infinie.
TypeInitializationException Levée quand un constructeur statique lève une exception et qu'il n'existe aucune clause catch pour l'intercepter.


Gérer une exception avec try/catch

L'objectif d'un bloc try-catch est d'intercepter et de gérer une exception générée par du code opérationnel. Certaines exceptions peuvent être gérées dans un bloc catch et le problème résolu sans que l'exception soit levée à nouveau, mais le plus souvent la seule chose que vous puissiez faire est de vous assurer que l'exception appropriée est levée.

Exemple

Dans cet exemple, IndexOutOfRangeException n'est pas l'exception la plus appropriée : ArgumentOutOfRangeException a de plus de sens pour la méthode car l'erreur est provoquée par l'argument index passé par l'appelant.


Commentaires

Le code qui provoque une exception est cloisonné dans le bloc try. Une instruction catch est ajoutée juste après pour gérer IndexOutOfRangeException, si elle se produit. Le bloc catch gère IndexOutOfRangeException et lève l'exception plus appropriée ArgumentOutOfRangeException à la place. Pour fournir à l'appelant autant d'informations que possible, pensez à spécifier l'exception d'origine comme InnerException de la nouvelle exception. Comme la propriété InnerException a la valeur readonly, vous devez l'affecter dans le constructeur de la nouvelle exception.

Exécuter le code de nettoyage avec finally

L'objectif d'une instruction finally est de vérifier que le nettoyage nécessaire des objets, généralement ceux contenant des ressources externes, se produit immédiatement, même si une exception est levée. Un exemple de nettoyage de ce type est l'appel à Close sur un FileStream immédiatement après son utilisation au lieu d'attendre que l'objet soit récupéré par garbage collection par le Common Language Runtime, comme suit :


Exemple

Pour transformer le code précédent en instruction try-catch-finally, le code de nettoyage est séparé du code actif comme suit.

Comme une exception peut se produire à tout moment dans le bloc try avant l'appel à OpenWrite(), ou comme l'appel OpenWrite() lui-même peut échouer, nous n'avons aucune garantie que le fichier est ouvert quand nous essayons de le fermer. Le bloc finally ajoute un contrôle pour garantir que l'objet FileStream n'est pas null avant d'appeler la méthode Close. Sans le contrôle null, le bloc finally pourrait lever sa propre NullReferenceException, mais la levée d'exceptions dans des blocs finally doit être évitée, dans la mesure du possible.

Une connexion de base de données est également susceptible d'être fermée dans un bloc finally. Le nombre de connexions à un serveur de base de données étant parfois limité, vous devez fermer les connexions de base de données le plus rapidement possible. Si une exception est levée avant que vous puissiez fermer votre connexion, c'est un autre cas où il vaut mieux utiliser le bloc finally qu'attendre le nettoyage de la mémoire.

Intercepter une exception non-CLS

Certains langages .NET, dont C++/CLI, permettent aux objets de lever des exceptions qui ne dérivent pas d'Exception. De telles exceptions sont appelées exceptions non-CLS ou non exceptions. Dans C#, vous ne pouvez pas lever d'exceptions non-CLS, mais vous pouvez les intercepter de deux façons :

Dans un bloc catch (RuntimeWrappedException e).
Par défaut, un assembly Visual C# intercepte les exceptions non-CLS comme des exceptions encapsulées. Utilisez cette méthode si vous devez accéder à l'exception d'origine, qui est accessible via la propriété RuntimeWrappedException.WrappedException. La procédure située plus loin dans cette rubrique explique comment intercepter les exceptions de cette manière.

Dans un bloc catch général (bloc catch sans type d'exception spécifié) qui est placé après tous les autres blocs catch.
Utilisez cette méthode lorsque vous souhaitez effectuer une action (comme écrire dans un fichier journal) en réponse à des exceptions non-CLS, et que vous n'avez pas besoin d'accéder aux informations de l'exception. Par défaut, le common language runtime inclut dans un wrapper toutes les exceptions. Pour désactiver ce comportement, ajoutez cet attribut d'assembly à votre code, généralement dans le fichier AssemblyInfo.cs : [assembly: RuntimeCompatibilityAttribute(WrapNonExceptionThrows = false)].

Comment intercepter une exception non-CLS

Dans un bloc catch(RuntimeWrappedException e), accédez à l'exception d'origine via la propriété RuntimeWrappedException.WrappedException.

Exemple

L'exemple suivant montre comment intercepter une exception non-CLS levée à partir d'une bibliothèque de classes écrite en C++/CLI. Notez que dans cet exemple, le code client C# sait par avance que le type d'exception levé est un System.String. Vous pouvez caster la propriété RuntimeWrappedException.WrappedException en son type d'origine, tant que celui-ci est accessible à partir de votre code.