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.
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:
Une méthode doit gérer les exceptions qui peuvent se produire dans les instructions qu’elle utilise.
Une exceptions non gérée sera ignorée par le programme.
Certaines exceptions ne sont pas causées par des bug de programmation.
L’expression
1/0
génère une exception.L’expression
1.0/0.0
génère une exception.Le bloc
finally
est optionnel après un bloctry/catch
.Le bloc
catch
doit apparaitre au moins une fois après un bloctry
.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:
Écrivez une énumération
Enseigne
qui permet de représenter les 4 enseignes : Pique, Coeur, Carreau et Trèfle.É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.Écrivez une classe
Carte
. Cette classe doit posséder 3 propriétés et 1 constructeur :
Une propriété
EnseigneCarte
de typeEnseigne
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)))
.