Show this page in english

Gestion des erreurs et des exceptions

Publié le 2 Janvier 2007

1. Introduction

Vos programmes doivent toujours être capable de traiter les erreurs et ceci afin d'en permettre une utilisation plus confortable.

Imaginez un programme ne traitant pas les erreurs : au moindre petit problème non prévu par le programmeur, le programme planterait, les données seraient très certainement perdues et vous n'auriez plus qu'à le relancer et à tout reprendre...
Par exemple, un programme qui demande à l'utilisateur de choisir un fichier afin d'en modifier quelques données. Si l'utilisateur choisit un fichier protégé en écriture et que votre application n'est pas en mesure de gérer ce cas là... c'est le drame et il faut tout refaire...

Heureusement, C# offre des possibilités sophistiquées pour traiter ce type de problème : il s'agit du mécanisme de gestion des exceptions.

Remarque

Les fonctionnalités de gestion d'erreurs en VB sont très limitées et reposent surtout sur l'utilisation de l'instruction On Error GoTo. Les anciens de VB remarqueront que les exceptions en C# ouvrent un mode nouveau dans la manière de gérer les erreurs.

Ceux qui viennent de Java et de C++ connaissent probablement le principe des exceptions puisque ces langages et C# traitent le sujet de manière similaire.

Les développeurs C++ sont parfois circonspects vis-à-vis des exceptions et de la possible chute de performances qu'elles peuvent entrainer. Mais ce n'est pas le cas en C# : de manière générale, l'utilisation d'exceptions dans le code C# n'affecte pas défavorablement les performances.

2. Capture et levée d'exceptions

2.1 Capture d'exceptions

Pour capturer des exceptions, il faut utiliser les blocs try, catch et finally dans un ordre bien précis :

try { //Code d'exécution normal } catch { //Gestion des erreurs } finally { //Nettoyage }

Le bloc try contient le code qui effectue les opérations normales du programme, mais qui risque de rencontrer des erreurs plus ou moins innatendues.

Le bloc catch contient le code de traitement des erreurs, erreurs qui sont apparues lors de l'exécution du programme.

Le bloc finally contient le code qui nettoie les ressources ou que vous voulez impérativement exécuter après le bloc try ou catch.
Ce bloc est exécuté qu'une exceptions soit levée ou pas !
Il est interdit d'utiliser l'instruction return dans un bloc finally : le compilateur signale une erreur.

Le schéma de fonctionnement est le suivant :

2.2 Levée d'exceptions

Alors comment cela fonctionne-t-il ? Comment le runtime sait-il qu'il doit aller dans un bloc catch (ou pas) en cas d'erreur ?

Lorsqu'une erreur est détectée, le code lève une exception :

throw new Exception();

Et c'est à ce moment là que l'exécution normale du bloc try s'arrête pour passer dans le bloc catch approprié :

catch (Exception e) { }

2.3 Quelques classes d'exceptions

Le Framework offre beaucoup de classes gérant les exceptions dont :

Les classes d'exceptions sont des objets : la classe System.Exception hérite de la classe System.Object et toutes les autres classes d'exceptions héritent directement ou indirectement de la classe System.Exception.
Par exemple, la classe ArgumentNullException hérite de ArgumentException qui hérite elle-même de SystemException qui (enfin) hérite de la classe de base Exception.

La classe Exception étant une classe de base, celle-ci traite toutes les exceptions possibles.

Lorsque votre application s'exécute avec en arrière plan le runtime .NET, votre fonction Main est en réalité imbriqué dans un gros bloc try (créé par le runtime) qui prévoit la levée d'une exception (Exception) : cela évite ainsi le fameux crash de l'application évoqué dans l'introcdution.

La classe ApplicationException est une classe héritant de la classe Exception et servira à créer vos propres classes d'exceptions. Vous verrez plus loin comment faire.

Dans le schéma suivant, toutes les classes appartiennent à l'espace de nom System, excepté IOException et ses dérivées qui se trouvent dans System.IO...

Schéma de quelques classes d'exceptions .NET

2.4 Exemples

Exemple :

using System;

public class Program { public static void Main() { int nombre = 0; bool quit = false; while (!quit) { Console.Write("Entrez un nombre : "); string chaine = Console.ReadLine(); try { /* Code à risque : il se peut que * l'utilisateur n'entre pas un * nombre et qui plus est, que ce * nombre ne soit pas un entier * compris entre - 2 147 438 648 et * + 2 147 438 647 */ nombre = Int32.Parse(chaine); quit = true; } catch (ArgumentException argEx) { Console.WriteLine(argEx.Message); } catch (FormatException formEx) { Console.WriteLine(formEx.Message); } catch (OverflowException overEx) { Console.WriteLine(overEx.Message); } } } }

Ce petit exemple demande tout simplement à l'utilisateur d'entrer un nombre et convertit la chaîne de caractères retournée en un entier signé sur 32 bits.

La documentation XML de la fonction Parse de la classe Int32 nous informe que cette méthode peut lever quatre exceptions différentes :

Vous remarquerez que :

Dans l'exemple qui suit, on considère que l'on se trouve dans une méthode membre d'une classe Customer qui doit renvoyer un objet Customer en fonction d'un numéro (ID). Si ce numéro n'est pas trouvé, on lève une exception fournissant des informations sur la nature du problème rencontré.

public Customer GetCustomerFrom(uint id) { bool find = false; Customer c = null; //Recherche du Customer, si celui-ci est trouvé, find passe à true. try { if (!find) throw new Exception(String.Format("L'ID {0} n'existe pas !", id)); } catch (Exception e) { MessageBox.Show(e.Message); } return c; }

Remarque :

Dans cet exemple, la levée d'une exception doit être faite en sachant ce que le reste du programme fait et surtout d'où provient cette ID. En effet, si cette ID a été entrée par l'utilisateur, alors la levée d'une exception est inutile car le problème peut être résolu par l'utilisateur (une petite condition d'existence de l'ID et si celle-ci n'existe pas, on en demande une autre à l'utilisateur).

En revanche, si cette ID provient directement du programme (génération automatique de l'ID, effaçage imprévu de données, etc.), dans ce cas, l'utilisateur n'étant pour rien la cause du problème, il faut lever une exception pour lui signaler qu'une erreur (imprévue, anormale) s'est produite.
Cette exception sera capturée dans le code client (pas dans la classe Customer (déconseillé ou avec modération)) et le message pourra ainsi être affiché à l'utilisateur.

Les blocs try peuvent jouer un rôle significatif dans le contrôle du flux d'exécution du code mais gardez à l'esprit qu'elles sont là pour traiter des cas exceptionnels, d'où leur nom... Par exemple, utiliser des exceptions comme moyen de contrôler la sortie d'une boucle do... while est considéré comme une très mauvaise pratique de programmation.

2.5 Quelques propriétés de la classe System.Exception

Voyons quelques une des propriétés de la classe System.Exception :

2.6 Blocs try imbriqués

Il est possible d'imbriquer des blocs try :

try { //Point A try { //Point B } catch { //Point EB } finally { //Point NB } } catch { //Point EA } finally { //Point NA }

Faisons rapidement un petit tour dans ce morceau de code...

Si une exception est levée dans le premier bloc try (point A), celle-ci est récupérée dans le bloc catch au point EA et s'ensuit comme toujours le nettoyage des ressources en NA. Si aucune exception n'est levée en A, alors le flux d'exécution poursuit dans le second bloc try (point B).

Si une exception est levée dans le bloc try interne et que le bloc catch associé est en mesure de traiter le problème, alors l'exécution se poursuit dans ce bloc catch (point EB) sinon, si le bloc EB ne peut contenir le problème mais le bloc EA oui, alors l'exécution continue dans le bloc EA.

En cas d'erreur dans le bloc catch ou finally interne, l'exception est récupérée et si le bloc catch EA peut la traiter, alors le flux d'exécution continue dans ce dernier et se termine par le bloc finally NA.

Si aucun des blocs catch de votre code n'est en mesure d'intercepter les exceptions levées, le runtime .NET s'en charge. En effet, comme nous l'avons déjà mentionné plus haut, lorsque votre application .NET est lancée par le runtime, celle-ci est en réalité imbriqué dans un énorme bloc try (du runtime) capable d'intercepter toute sorte d'exceptions.

En général, efforcez vous lorsque vous codez un exécutable de traiter intelligemment un maximum d'exceptions. Si vous écrivez une bibliothèque, il est préférable de ne pas traiter les exceptions mais seulement de les lever (throw new ...) ou sinon, de capturer les exceptions Microsoft pour ensuite lever des exceptions personnalisées fournissant davantages d'informations.

3. Création d'exceptions personnalisées

3.1 Les exceptions sont des classes .NET

Les exceptions étant toutes des classes .NET, il suffit pour créer sa propre classe d'exception de créer une classe qui héritera d'une classe de base d'exception (déjà fournie ou non pas le Framework).

Lorsque vous créez vos classes d'exceptions, il est conseillé de les dériver à partir de ApplicationException (de l'espace System) qui dérive lui même directement de System.Exception, à moins que votre classe d'exception de base soit plus aproprié comme étant un FileNotFoundException ou autre... Cela dépend bien entendu de ce que vous voulez faire.

La structure de la classe est donc la suivante :

public class MyException : ApplicationException { //Champs personnalisées //Constructeurs personnalisées //Propriétés personnalisées //Méthodes personnalisées }

3.2 Lever une exception personnalisée

Pour lever une exception que vous avez vous même implémentée, c'est toujours la même chose, vous devez utiliser throw puis construire votre classe d'exception.

On peut donc directement passer à un exemple puisqu'il n'y a rien de difficile...

3.3 Exemple

Examinez puis testez le code suivant...

using System;
using System.IO;
using System.Text;
using System.Collections.Generic;
using Customers;

namespace CodeClient { public class Program { private static string path = @"C:\customers.txt"; public static void Main() { Customer[] c = new Customer[5]{ new Customer(1000, "Michel"), new Customer(9999, "George"), new Customer(3485, "Matthieu"), new Customer(666, "Jean"), new Customer(1, "Maxime") }; CustomerList cl = new CustomerList(c); cl.Save(path); Console.WriteLine(cl.ToString()); try { //Existe, pas de problème Console.WriteLine(cl.GetCustomerWhereIdIs(1000).ToString()); //Lève une exception //Console.WriteLine(cl.GetCustomerWhereIdIs(2).ToString()); //Ne modifiez pas le fichier la première (pour voir que tout se passe bien) //Modifiez le la 2e fois... Console.WriteLine("Conneries time : validez pour continuer..."); Console.ReadLine(); //Fonctionne normalement si le fichier n'a pas été modifié if (cl.Open(path)) Console.WriteLine("Ouverture du fichier avec succès"); Console.WriteLine(cl.ToString()); } catch (CustomerException e1) { Console.WriteLine(e1.Message); } catch (CustomerListFileFormatException e2) { Console.WriteLine(e2.Message); } Console.WriteLine("Good bye"); Console.ReadLine(); } } } namespace Customers { public struct Customer
{ //Champs private int id; private string name; //Constructeur public Customer(int id, string name) { this.id = id; this.name = name; } //Propriétés public int ID { get { return id; } } public string Name { get { return name; } set { name = value; } } public static Customer GetCustomerFrom(byte[] array, byte sepByte) { if (array[4] != sepByte) throw new CustomerFileFormatException("Un problème est apparu lors de la lecture d'un des des clients !"); int id = array[0] << 24 | array[1] << 16 | array[2] << 8 | array[3]; string name = Encoding.Default.GetString(array, 5, array.Length - 5); return new Customer(id, name); } public byte[] GetBytes(byte sepByte) { byte[] array = new byte[5 + name.Length]; array[0] = (byte)(id >> 24); array[1] = (byte)((id >> 16) & 0xFF); array[2] = (byte)((id >> 8) & 0xFF); array[3] = (byte)(id & 0xFF); array[4] = sepByte; Encoding.Default.GetBytes(name, 0, name.Length, array, 5); return array; } public override string ToString() { return String.Format("{0} - {1}", id, name); } } public class CustomerList { private List<Customer> customerList; private static byte sepByte = 0xEE; public CustomerList() { customerList = new List<Customer>(); } public CustomerList(Customer[] list) { customerList = new List<Customer>(list); } public Customer GetCustomerWhereIdIs(int id) { foreach (Customer c in customerList) if (c.ID == id) return c; throw new CustomerException("Le numéro du client demandé n'existe pas !", id, null); } public void Save(string path) { /* Création d'une liste de bytes. Estimation de la taille : * un entier fait 4 bytes, le nom ne devrait pas dépasser * 20 caractères en général + 2 caractères séparateurs. */ List<byte> array = new List<byte>(customerList.Count * 25); foreach (Customer c in customerList) { array.AddRange(c.GetBytes(sepByte)); //Double caractère pour séparer les différents clients entre eux. array.Add(sepByte); array.Add(sepByte); } FileStream fs = File.Create(path, 16); fs.Write(array.ToArray(), 0, array.Count); fs.Close();
} public bool Open(string path) { customerList.Clear(); if (!File.Exists(path))
throw new FileNotFoundException("Le fichier spécifié n'existe pas !", path); //On récupère les données byte[] array = File.ReadAllBytes(path); /* On ne vérifie même pas le format, on lit directement les données * (il est quand même préférable de vérifier que le fichier est aux * normes, mais cela rajoute un certain nombre de lignes de codes * inutiles dans cet exemple. */ int counter = 0; int startIndex = 0; for (int i = 0; i < array.Length; i++) { if (array[i] == sepByte)
counter++; //Au bout de 3 séparateurs, on tient notre Customer if (counter == 3) { byte[] table = new byte[i - 1 - startIndex]; Array.Copy(array, startIndex, table, 0, table.Length); try { customerList.Add(Customer.GetCustomerFrom(table, sepByte)); } catch (CustomerFileFormatException e) { throw new CustomerListFileFormatException("Erreur", i, array, e); } startIndex = ++i; counter = 0; } } return true; } public override string ToString() { StringBuilder sb = new StringBuilder(); foreach (Customer c in customerList) sb.AppendLine(c.ToString()); return sb.ToString(); } } public class CustomerException : ApplicationException { private int id; public CustomerException(string message) : base(message) { id = -1; } public CustomerException(string message, Exception innerException) : this(message, -1, innerException) { } public CustomerException(string message, int id, Exception innerException) : base(message, innerException) { this.id = id; } public int ID { get { return id; } } } public class CustomerListFileFormatException : ApplicationException { private int errorLocation; private byte[] data; public CustomerListFileFormatException(string message, int errorLocation, byte[] data) : this(message, errorLocation, data, null) { } public CustomerListFileFormatException(string message, int errorLocation, byte[] data, Exception innerException) : base(message, innerException) { this.errorLocation = errorLocation; this.data = data; } public int ErrorLocation { get { return errorLocation; } } public byte[] BinaryData { get { return data; } } } public class CustomerFileFormatException : ApplicationException { public CustomerFileFormatException(string message) : base(message) { } public CustomerFileFormatException(string message, Exception innerException) : base(message, innerException) { } } }

Voila, je pense que le code est suffisemment clair et que les explications précédentes devraient suffir à bien comprendre le principe de fonctionnement.

3.4 Dernières explications

Dans l'exemple précédant, les exceptions étaient chargées de capturer les erreurs associées au format du fichier (mauvais format) ou à d'autres problèmes plus généraux liés à la classe CustomerList.

Vous aurez peut être remarqué qu'aucune exception n'est traitée dans l'espace de nom Customers, sauf une toute petite mais qui en crée une autre immédiatement fournissant plus d'informations (ligne 193 ou deuxième bloc try de l'exemple). L'exception levée précédemment (CustomerFileFormatException) n'est pas perdue : celle-ci est passé en paramètre au constructeur de CustomerListFileFormatException : il s'agit du paramètre innerException dont la vocation est justement de contenir l'exception parente (qui peut avoir elle même d'autres exceptions parentes) de celle en cours d'instanciation.

De manière générale, lorsque vous codez en .NET, ne déclarez pas un bloc catch sans paramètre (mettez au moins Exception). Par contre, si vous faîtes du code non managé, vous n'avez pas le choix, vous ne pouvez utiliser une classe .NET dans ce type de code et ne pouvez donc pas instancier d'exception. Mais vous pouvez quand même les gérer :

try { } catch { } finally { }