Compléments de langage

Exceptions

Une exception désigne une erreur pouvant se produire lors de l’exécution d’un programme. En C# il existe deux sources d’exception :

  • les exceptions systèmes (runtime): division par zéro, accès en dehors d’un tableau, utilisation d’une référence null. Ces exceptions sont le résultat d’une erreur de programmation (bug).

  • les exceptions générées volontairement par le programmeur pour signaler un problème.

Historiquement, les exceptions étaient gérées par le programmeur au moyen de code d’erreur (par exemple en C): cette approche est lourde et manque de fiabilité, une stratégie plus systématique et gérée directement par le compilateur a donc été proposée dans les langages modernes (C#, Java, C++…).

Le mécanisme de gestion des exceptions reposent sur les principes suivants:

  • Toutes les fonctions peuvent lever/lancer une exception.

  • Toutes les fonctions peuvent déclarer des blocs de gestion d’exceptions.

  • Lorsqu’une exception est levée, toutes les fonctions de la pile d’exécution sont terminées jusqu’à en trouver une capable de gérer cette exception. Si une telle fonction existe, l’exécution reprend dans le bloc de gestion d’exception de la fonction trouvée.

  • Si aucune fonction n’est capable de gérer l’exception le programme est arrêté.

Considérons par exemple ce qui pourrait se produire lors de la sauvegarde d’une image dans un logiciel de dessin. Lorsque l’utilisateur clique sur le bouton sauvegarder, la fonction SauvegarderIHM est appellée. Cette fonction affiche une boite de dialogue pour que l’utilisateur choisisse un fichier. Lorsque l’utilisateur a choisi un fichier de sauvegarde, le programme appel la fonction SauvegarderImage. Cette fonction va convertir l’image dans un format standard (par exemple jpg) puis va appeler la fonction EcrireFichier qui permet d’écrire des données dans un fichier. Supposons maintenant que le disque dur de l’utilisateur soit plein. La fonction EcrireFichier lève une exception pour prévenir du problème. Son exécution est stoppée et le CLR regarde si la fonction qui l’avait appelée, c’est-à-dire SauvegarderDocument sait gérer l’exception. A priori cette fonction ne peut rien faire pour résoudre le problème et ne la gèrera pas. Le système remonte ensuite à la fonction SauvegarderIHM, qui elle peut gérer l’exception en demandant à l’utilisateur de choisir un autre emplacement de sauvegarde.

../_images/exception.svg

Avec un tel mécanisme il n’est pas possible d’interrompre (non intentionnellement) la propagation d’une exception.

Gérer une exception

Pour gérer une exception on utilise les blocs try/catch. Le bloc try (essayer) comporte le code susceptible de générer des exceptions et le bloc catch (attraper) contient le code de gestion des exceptions.

int a,b,c;
...
try { // bloc try
        a = b/c ;       // erreur division par 0 si c==0
}
catch (Exception e){ // bloc catch
        Console.WriteLine("Erreur! {0}", e.Message);
}

Le bloc catch récupère un objet de type Exception qui contient une Propriété Message qui décrit l’exception.

Il existe différentes classes d’exceptions qui descendent de la classe Exception et il est possible d’affecter des blocs de traitement différents pour chaque exception.

int a,b,c;
int [] t;
...
try {
        t[a] = b/c ;
}
catch (AritmeticException e){
        Console.WriteLine("Erreur sur une opération arithmétique! {0}", e.Message);
}
catch (IndexOutOfRangeException e){
        Console.WriteLine("Accès en dehors des bornes d'un tableau! {0}", e.Message);
}
catch (Exception){
        Console.WriteLine("Erreur!");
}

Il y a donc des exceptions plus ou moins spécialisées, ce qui permet de ne gérer que certaines exceptions. Le type Exception désigne n’importe qu’elle exception et attrapera donc toutes les exceptions possibles.

Attention car l’ordre des catch est important pour savoir quel bloc va gérer l’exception. Par exemple, dans le code :

try {
        ...
}
catch (Exception){
        Console.WriteLine("Erreur sur une opération arithmétique!");
}
catch (IndexOutOfRangeException){
        Console.WriteLine("Accès en dehors des bornes d'un tableau");
}

Le bloc catch lié à l’exception IndexOutOfRangeException ne sera jamais atteint car le bloc précédent attrapera toutes les exceptions possibles.

Tous les objets décrivant une Exception possèdent les propriétés suivantes:

  • Message : description de l’exception

  • StackTrace : état de la pile d’exécution (liste des fonctions qui ont menées à cette exception)

  • Source : nom de l’application ou de l’objet qui a levé l’exception

  • TargetSite : nom de la méthode qui a levé l’exception

  • InnerException : exception à l’origine de l’exception (si non null)

Lancer une exception

On lance une exception avec le mot clé throw suivi d’un objet de type Exception (ou un descendant). Par exemple:

double Racine(double a){
        if(a<0)
                throw new ApplicationException("Erreur : la racine carrée d'un nombre négatif n'existe pas.");
        return Math.Sqrt(a);
}

Bloc finally

Chaque bloc try peut être associé à un bloc finally. Les instructions placées dans le bloc finally seront exécutées même si une exception se produit dans le bloc try, que cette exception soit gérée ou non. Ce bloc est très util pour gérer la libération des ressources tenues. Par exemple:

StreamWriter sw = null; // pour écrire dans un fichier
try {
        sw = new StreamWriter("y:\\monfichier.txt"); // ressource système vers un fichier
        sw.WriteLine("toto");
} catch (Exception e) {
        Console.WriteLine("Erreur : {0}", e.Message);
} finally { // bloc finally
        // ce block est toujours executé : c'est le bon endroit pour fermer le fichier !
        if (sw != null) // si la construction du StreamWriter a échoué alors sw est null : le fichier n'a jamais été ouvert
                sw.Close(); // fermer le fichier
}

On peut même avoir un bloc finally sans bloc catch:

StreamWriter sw = null;
try {
        sw = new StreamWriter("y:\\monfichier.txt");
        sw.WriteLine("foo");
} finally {
        if (sw != null)
                sw.Close();
}

Exercices

Pour chacune de ces affirmations, indiquez si elle est vraie ou fausse:

  1. Une méthode doit gérer les exceptions qui peuvent se produire dans les instructions qu’elle utilise.

  2. Une exceptions non gérée sera ignorée par le programme.

  3. Certaines exceptions ne sont pas causées par des bug de programmation.

  4. L’expression 1/0 génère une exception.

  5. L’expression 1.0/0.0 génère une exception.

  6. Le bloc finally est optionnel après un bloc try/catch.

  7. Le bloc catch doit apparaitre au moins une fois après un bloc try.

  8. Après une instruction throw, la fonction reprend son exécution normalement si l’exception a été gérée par une autre fonction.

class Test{
        void H(bool f){
                Console.Write("H1");
                if(f)
                        throw new Exception("e1");
                Console.Write("H2");
        }

        void G(bool f){
                Console.Write("G1");
                try{
                        Console.Write("G2");
                        H(f);
                        Console.Write("G3");
                } catch (IndexOutOfRangeException e)
                { Console.Write("G4"); }
                finally
                { Console.Write("G5"); }
                Console.Write("G6");
        }

        void F(bool f){
                try{
                        Console.Write("F1");
                        G(f);
                        Console.Write("F2");
                } catch (Exception e) { Console.Write("F3"); }
                finally { Console.Write("F4"); }
                Console.Write("F5");
        }
}

L’appel à la méthode F(false) affiche la chaine de caractères

L’appel à la méthode F(true) affiche la chaine de caractères

class Test{
        int f(int x){
                if(x<0)
                        throw new ApplicationException("");
                Console.Write("fin f ");
                return 1;
        }

        int g(int x){
                int res= f(x) + 1;
                Console.Write("fin g ");
                return res;
        }

        public static void Main(String [] args) {
                try{
                        int res=g(-1);
                        Console.Write("try " + res + " ");
                } catch (ApplicationException e1)               {
                        Console.Write("catch E1 ");
                } catch (Exception e2)          {
                        Console.Write("catch E2 ");
                } finally {
                        Console.Write("finally");
                }
        }
}

Ce programme affiche la chaine de caractères

class A{
        static int a=10;
        static int [] t = new int[3];
        static int i, j;

        static void F(){
                try{
                        Console.Write("F1");
                        t[i]=a/j;
                        Console.Write("F2");    }
                catch(ArithmeticException) { Console.Write("F3");       }
                finally{ Console.Write("F4");   }
                Console.Write("F5");
        }

        static void G(){
                try{
                        Console.Write("G1");
                        F();
                        Console.Write("G2");    }
                catch(ArithmeticException) { Console.Write("G3");       }
                catch(IndexOutOfRangeException) { Console.Write("G4");  }
                finally { Console.Write("G5");  }
                Console.Write("G6");
        }

        static void Main(String args){
                i=1;    j=1;
                G();
                i=1;    j=0;
                G();
                i=3;    j=1;
                G()
        }

Le premier appel à la fonction G affiche la chaine de caractères .

Le deuxième appel à la fonction G affiche la chaine de caractères .

Le troisième appel à la fonction G affiche la chaine de caractères .

Enumérations

Une énumérations est un type de données dont les valeurs sont données par un liste de noms symboliques. Imaginons que l’on veuille représenter l’état civil d’une personne qui peut prendre sa valeur parmi : Célibataire, Marié, Divorcé ou Veuf. Une solution simple consiste à définir des constantes numériques représentant ces différentes possibilités :

public const byte CELIBATAIRE = 0; // const signifie que la variable est une constante
public const byte MARIE = 1;
public const byte DIVORCE = 3;
public const byte VEUF = 4;

Mais cette solution est risquée car un état civil est représenté par un byte et rien n’empêche le programmeur d’y mettre une valeur qui ne représente aucun état civil valide (par exemple 8).

Les énumérations fournissent une solution élégante à ce problème. Elles permettent de définir un nouveau type EtatCivil qui prend ses valeurs parmi la liste de valeurs Célibataire, Marié… Cette définition est réalisée grâce au mot clef enum suivi de l’identificateur de l’énumération, puis de l’ensemble des valeurs qu’elle peut prendre mises entre accolades et séparées par des virgules.

enum EtatCivil {Celibataire, Marie, Divorce, Veuf} // définition du type EtatCivil
...
EtatCivil ec ; // définition d’une variable de type EtatCivil
ec = EtatCivil.Marie ; // modification de la valeur

On accède aux valeurs de l’énumération comme aux champs statiques d’une classe : IdentificateurEnumeration.Valeur. La valeur par défaut d’une variable d’un type énumération correspond au premier élément de la liste des valeurs.

Exemple d’utilisation dans un switch pour déterminer le nombre de parts intervenant dans le calcul de l’impôt sur le revenu :

EtatCivil ec = ... ;
...
double nombreDeParts = 0 ;
switch (ec)
{
        case EtatCivil.Celibataire :
                nombreDeParts = 1 ;
                break ;
        case EtatCivil.Marie :
                nombreDeParts = 2 ;
                break ;
        case EtatCivil.Divorce :
                nombreDeParts = 1 ;
                break ;
        case EtatCivil.Veuf :
                nombreDeParts = 1.5 ;
                break ;
}

On souhaite écrire des classes permettant de représenter un jeu de cartes à jouer. Un tel jeu est construit de la manière suivante : Le jeu comprend 52 cartes à jouer, réparties suivant:

  • treize valeurs : As, Roi, Dame, Valet, et les points (2, 3, 4, 5, 6, 7, 8, 9 et 10) ;

  • quatre enseignes : pique, coeur, carreau et trèfle.

Dans un nouveau projet Console sous Visual Studio:

  1. Écrivez une énumération Enseigne qui permet de représenter les 4 enseignes : Pique, Coeur, Carreau et Trèfle.

  2. Écrivez une énumération Valeur qui permet de représenter les 13 valeurs : As, Roi, Dame, Valet, Deux, Trois, Quatre, Cinq, Six, Sept, Huit Neuf, Dix.

  3. Écrivez une classe Carte. Cette classe doit posséder 3 propriétés et 1 constructeur :

    • Une propriété EnseigneCarte de type Enseigne avec un getter et un setter publics qui permettent de connaître et modifier l’enseigne de la carte ;

    • Une propriété ValeurCarte de type Valeur avec un getter et un setter publics qui permettent de connaître et modifier la valeur de la carte ;

    • Une propriété Points de type entier avec un getter qui donne le nombre de points de la carte. On considère que le nombre de points des cartes de valeurs As, Roi, Dame et Valet est égal à 10.

    • Un constructeur qui prend en paramètre une Enseigne et une Valeur et qui initialise les propriétés avec les valeurs des paramètres.

  • Écrivez la classe Jeu. Cette classe possède :

    • Un champ privé de type tableau de carte nommé cartes ;

    • Un constructeur public sans paramètre qui remplit le tableau cartes avec les 52 cartes du jeu. Indication : il est possible de parcourir les éléments de l’énumération Enseigne avec une boucle foreach de la manière suivante : foreach(Enseigne variable in Enum.GetValues(typeof(Enseigne))).