Archives Wild Code School
C#

5. Les exceptions

Quête 31 sur 46

Après cette quête, tu sauras utiliser des exceptions

Objectifs

  • Savoir gérer les exceptions
  • Savoir créer une exception custom
  • Savoir utiliser son excepion custom

Introduction

En tant que bon développeur, tu écris du code parfait. Propre, sans bug, et entièrement sécurisé. Mais comme disait Sartre, L'enfer, c'est les autres.

Par conséquent, il faut prendre en compte que parfois, des erreurs peuvent se glisser dans le code (des autres hein, pas le nôtre !). Si rien n'est fait pour gérer ces erreurs, ça peut aboutir à un plantage total de l'application et à son arrêt.

Même en admettant que l'équipe soit entièrement composé de bons développeurs comme toi, il reste une faille possible : les utilisateurs !

Prenons un exemple simple : je demande à l'utilisateur de saisir un nombre, et en bon utilisateur lambda, il saisit la valeur a;

Horreur, malheur, en faisant le parse de sa saisie avec un int value = Int32.Parse(userInput);, le programme crash en expliquant que a n'est pas un nombre (logique tu me dira).

Tu as peut être vu que pour palier à ce problème, il existe la méthode Int.TryParse, qui fonctionne un peu différemment et ne génère pas d'erreur dans notre cas.

Cette fonction m'amène au principe fondamental de la gestion des erreurs en programmation : les blocs try/catch

Try

Le principe du bloc try, comme son nom l'indique, est d'essayer de réaliser une liste d'instructions. Si une des instructions génère une erreur, les suivantes sont ignorées, et le programme te laisse la possibilité de gérer l'erreur. Exemple :

cs
try
{
	string input = "a";
	int value = Int32.Parse(input);
	Console.WriteLine(value);
}

Console.WriteLine("Fin du programme");

Le parse ne va pas fonctionner, et donc la valeur ne sera pas affichée. Par contre le message Fin du programme sera correctement affiché.

Ok l'erreur est ignorée, mais elle n'est pas gérée là si ?

Non, tu as raison. Le try sert à déclarer le bloc qu'on veut exécuter si tout va bien. Pour gérer l'erreur, il faut ajouter un bloc catch

Catch

Le principe de ce bloc est de dire si une erreur survient, voilà ce qu'il faut faire pour la gérer. En reprenant l'exemple d'avant, ça donne :

cs
try
{
	string input = "a";
	int value = Int32.Parse(input);
	Console.WriteLine(value);
}
catch (Exception e)
{
	Console.WriteLine("Une erreur est survenue lors du parse de la saisie");
}

Console.WriteLine("Fin du programme");

Avec cette nouvelle version, la conversion de a va toujours échouer, et donc la valeur ne sera pas affichée, mais en revanche le programme affichera Une erreur est survenue lors du parse de la saisie ainsi que Fin du programme !

Cette logique peut s'appliquer à tous les traitements à risque. Sa syntaxe ressemble un peu à un bloc if/else.

La classe Exception

Tu as pu voir dans l'exemple ci-dessus que lors de l'utilisation du catch, il a été spécifié entre parenthèse Exception e.

Exception correspond à une classe de base du langage, et e est le nom de la variable permettant de la représenter. Tu peux bien évidemment nommer cette variable comme bon te semble.

Cette exception correspond à l'erreur qui est survenue et contient plein d'informations utiles pour t'aider à comprendre ce qui s'est passé. Tu peux accéder aux détails de ces informations grâce aux propriétés de la variable e. Exemple :

cs
try
{...}
catch (Exception e)
{
	Console.WriteLine(e.Message);
}

Il y'a beaucoup d'informations supplémentaires disponibles, mais je te laisse consulter la documentation officielle pour ça.

↗
Les exceptions en .Net
learn.microsoft.com

Comment générer une exception soi-même ?

Les exceptions sont déjà utilisées par défaut par certaines actions du langage. C'est le cas de Int32.Parse justement.

Si tu veux réutiliser le même système, tu peux au sein d'une fonction utiliser le mot clé throw :

cs
public void myFunction()
{
	// ...

	if (inputNotOk)
	{
		throw new Exception("La saisie de l'utilisateur n'est pas valide");
	}

	// ...
}

Le mot clé throw fonctionne comme le mot clé return. Il met fin a l'exécution de la fonction. Les lignes suivantes ne sont donc pas exécutées.

Le message que tu souhaites transmettre est le paramètre principal du constructeur de la classe, et tu peux ensuite y accéder avec le fameux e.Message.

Contrairement au mot clé return, il fonctionne de la même manière peu importe le type de retour de la fonction. Il faut donc bien faire attention de ne pas écrire return new Exception(...); mais bien throw new Exception(...);

La particularité d'une exception, est que tant qu'elle n'est pas gérée (via un bloc catch), elle va remonter dans la pile d'appel, et donc stoper tout processus en cours. Il est donc important de bien ajouter les instructions try/catch quand vous utilisez des méthodes susceptibles de lever une exception.

Les exceptions par héritage

Je te disais avant que la fonction Int32.Parse pouvait lever une exception en cas d'erreur. En réalité ce n'est pas une Exception mais une FormatException.

FormatException est une classe qui hérite de Exception, et c'est ce qui explique que catch(Exception e) fonctionne. Tu peux donc écrire catch(FormatException e) dans l'exemple précédent pour arriver au même résultat.

Penchons nous sur la documentation officielle de la fonction TryParse, et notamment sur le bloc Exceptions qui explique les différents types d'exceptions que cette méthode peut renvoyer

↗
Dcumentation de Int32.Parse
learn.microsoft.com

Tu constates qu'en plus de FormatException qui est utilisé pour indiquer par exemple que a n'est pas un nombre, tu peux rencontrer une ArgumentNullException si le paramètre de Int32.Parse est null, ou OverflowException si le nombre a parser est trop grand.

Cibler une exception particulière

On a vu que la méthode Int32.Parse peut renvoyer 3 exceptions différentes. Il est ainsi possible d'utiliser plusieurs catch successifs pour gérer les erreurs de manière plus spécifiques. Exemple :

cs
try
{
	int value =Int32.Parse("abc");
}
catch (ArgumentNullException argumentNullException)
{
	Console.WriteLine("Le paramètre de Int32.Parse est null");
}
catch (FormatException formatException)
{
	Console.WriteLine("Le paramètre n'est pas un nombre");
}
catch (OverflowException overflowException)
{
	Console.WriteLine("Le nombre à parser est trop grand");
}

Tu peux ainsi cibler précisément le type d'erreur rencontré et adapter ton traitement en conséquent. Il est possible d'utiliser le type Exception comme gestion par défaut :

cs
try
{
	int value =Int32.Parse("abc");
}
catch (FormatException formatException)
{
	Console.WriteLine("Le paramètre n'est pas un nombre");
}
catch (Exception exception)
{
	Console.WriteLine("Une erreur est survenue : " + exception.Message);
}

Dans ce cas, c'est toujours le cas le plus spécifique qui sera pris en compte. Donc pour une erreur de format, seul Le paramètre n'est pas un nombre sera affiché. Dans les autres cas, le message générique Une erreur est survenue sera affiché.

Créer sa propre exception

Comme tu l'as compris, il existe déjà plusieurs classes représentant différentes exceptions possibles. Toutes ces classes héritent de la classe Exception, ce qui signifie que tu peux faire la tienne si tu respecte la même rêgle. Exemple :

cs
public class CustomException : Exception
{
	public CustomException(string message) : base(message)
	{

	}
}

Il suffit de créer au minimum le constructeur qui prend en paramètre le message, mais tu peux également implémenter d'autres constructeurs selon tes besoins.

Tu peux également ajouter toutes les propriétés que tu veux dans cette classe. C'est une exception, mais ça n'en reste pas moins une classe !

Il suffit ensuite de l'utiliser comme n'importe quelle exception :

cs
throw new CustomException("Houston ? On a un problème !");

Conclusion

Tu sais maintenant comment gérer proprement les erreurs qui peuvent survenir (parfois même de façon volontaire) lors de l'exécution de ton programme.

Il reste beaucoup de subtilités à découvrir sur leur utilisation et leur gestion, mais nous en reparlerons plus tard, et ça viendra petit à petit avec l'expérience.

Challenge

Tu vas devoir créer ta propre CustomException qui contient un message, ainsi qu'une propriété de type Date, contenant l'heure à laquelle l'exception à été levée (throw new...);

Dans ton programme principal, crée une fonction ThrowError() qui se contente de lever une CustomException avec les informations correspondantes.

Appelle cette fonction depuis le main en prenant soin de l'englober correctement d'un try/catch, et affiche la date et le message de l'exception dans la console.

Critères de validation

  • Un lien vers ta solution sur GitHub
  • La classe CustomException hérite de Exception
  • La classe CustomException possède une propriété supplémentaire de type Date, correctement initialisée à l'heure de sa création
  • La méthode ThrowError lève une CustomException
  • L'exception est correctement gérée dans la fonction main, et la date et le message s'affiche correctement