Une énumération (enum en C#) énumère, mais ne donne pas de garantie
S'il y a un aspect des énumérations qui est méconnu (et j'ai moi même fait ce type de mauvaise supposition par le passé), c'est bien le fait que l'utilisation d'un enum pour une propriété, un paramètre (etc) ne garanti PAS l'obtention d'une valeur valide de cette énumération.
Vous pouvez voir cette information en autres au travers de guidelines à propos de la définition d'énumération et de gestion de leur évolution, comme c'est le cas dans ce post de Krzysztof Cwalina, et dans le livre Framework Design Guidelines dont la seconde édition est sortie récemment.
Ces guidelines abordent divers sujets comme la nécessité de traiter de manière particulière la valeur 0, mais dans ce post nous allons nous pencher un peu plus sur cet aspect de non garantie de validité qu'il faut prendre en compte lors du développement d'API utilisant des énumérations, surtout si elle est publique.
Une énumération énumère, mais ne donne pas de garantie
Je pense que cette petite phrase peut aider à mémoriser ce caractère : "Une énumération énumère, mais ne donne pas de garantie"
En effet, le code suivant est tout à fait valide (tout du moins d'un point de vue compilation et exécution, mais côté logique ça peut coincer) :
class Program
{
/// <summary>
/// Liste des valeurs possibles.
/// </summary>
public enum ValeursPossibles : int
{
Un = 1,
Deux = 2,
Trois = 3
}
static void Main(string[] args)
{
// Normal
DoSomething(ValeursPossibles.Un);
DoSomething(ValeursPossibles.Deux);
DoSomething(ValeursPossibles.Trois);
// Déjà beaucoup moins normal
DoSomething((ValeursPossibles)(-1));
DoSomething((ValeursPossibles)(Int32.MinValue));
DoSomething((ValeursPossibles)(Int32.MaxValue));
// L'absence de l'attribut Flags n'exclu pas les opérations de ce type
DoSomething(ValeursPossibles.Un | ValeursPossibles.Deux );
DoSomething(ValeursPossibles.Un | (ValeursPossibles)256);
}
public static void DoSomething(ValeursPossibles valeur)
{
Console.WriteLine("DoSomething : {0} ({0:D})", valeur);
}
}
Ce qui donne la sortie :
DoSomething : Un (1)
DoSomething : Deux (2)
DoSomething : Trois (3)
DoSomething : -1 (-1)
DoSomething : -2147483648 (-2147483648)
DoSomething : 2147483647 (2147483647)
DoSomething : Trois (3)
DoSomething : 257 (257)
L'utilisation d'une énumération ne vous libère pas de la validation des valeurs attendues.
Il faut voir l'énumération comme une simplification lors de la saisie/lecture du code et un moyen de réduire les erreurs involontaires par présentation d'une liste de valeurs possibles facilement identifiables, tant par les développeurs que par leurs outils (contrairement à des solutions du type constantes).
Elle n'empêchera en rien un utilisateur malintentionné de tenter de faire des dégâts.
Comment valider ?
L'utilisation de Enum.IsDefined pour vérifier simplement la liste des valeurs définies par l'énumération est déconseillée par certaines guidelines, comme justement celles citées plus haut.
Cette méthode est déconseillée tant pour un éventuel problème de coût à l'exécution que pour le fait qu'elle ne permet pas de gérer certains aspects de cette validation, notamment celles liées à l'évolution possible de l'enum sans pour autant que l'ensemble du code l'utilisant évolue lui aussi.
A mon avis en validant les entrées, il faut se poser ce type de questions :
- est-ce que je dois autoriser n'importe quelle valeur définie dans l'énumération, y compris les ajouts futurs ?
- est-ce que je dois autoriser n'importe quelle valeur définie dans l'énumération, mais pas les ajouts futurs ?
- au moment d'écrire le code, une valeur de l'énumération est invalide dans mon cas : est ce que je dois autoriser n'importe quel ajout futur ?
Ce type de question va orienter votre choix de méthode de vérification, qui peut, je pense, être imagé de la manière suivante :
"Nous maitrisons, personne chez nous ne passera de valeur invalide !"
Et si un jour ça venait à être exposé au travers d'un service web ou d'un service WCF ?
Avec une valeur invalide, les proxies générés devraient échouer dans leur tentative d'envoi, par contre un message bâti à la main pourrait passer (dans le cas d'un service web la simple page de test permet d'envoyer une valeur invalide).
Dans tous les cas, une validation uniquement côté client ne peut être considérée comme suffisante.
Une simple vérification par plage peut prendre cette forme :
public static void DoSomethingSafe(ValeursPossibles valeur)
{
// Validation du paramètre valeur
if (valeur < ValeursPossibles.Un || valeur > ValeursPossibles.Trois)
{
throw new InvalidEnumArgumentException(
"valeur",
(Int32)valeur,
typeof(ValeursPossibles)
);
}
Console.WriteLine("DoSomething : {0} ({0:D})", valeur);
}
Et si je levais une exception en cas de valeur invalide ?
Vous l'avez sans doute vu dans l'exemple ci-dessus, si vous souhaitez lever une exception en cas de spécification de valeur invalide à votre API, au lieu d'utiliser une simple ArgumentException vous pouvez opter pour un type d'exception plus spécifique : InvalidEnumArgumentException
Le type InvalidEnumArgumentException est défini dans l'assembly System.dll (contrairement à ArgumentException défini directement dans mscorlib.dll), sous l'espace de nom System.ComponentModel.
Ce post vous a plu ? Ajoutez le dans vos favoris pour ne pas perdre de temps à le retrouver le jour où vous en aurez besoin :