Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

#if versus Conditional

[new:15/02/2011]La compilation conditionnelle n’est pas une grande nouveauté, les #if sont utilisés sous cette forme ou d’autres dans de nombreux langages depuis des temps immémoriaux... Sous C# nous disposons d’un outil de plus, l’attribut “Contional” qui reste à ma grande surprise méconnu, en tout cas fort peu utilisé. Réparons cette injustice et découvrons rapidement cet outil.

#if – la compilation conditionnelle

La compilation conditionnelle, en quelques mots, représente la capacité de certains compilateurs comme C#, et grâce à un marqueur introduit dans le code source, de pouvoir sauter des morceaux de codes qui ne seront pas compilés dans certaines conditions. Le cas le plus classique est le code de debug. Quand un code est ainsi instrumentalisé, plutôt que de fabriquer un nouveau code source pour la release qui serait débarrassée du code de debug, c’est ce dernier qui disparait automatique du code binaire tout en restant en place dans le code source.

La compilation conditionnelle est utilisée dans d’autres cas puisque, en général, et c’est le cas sous C#, on peut tester des conditions assez variées. Ainsi il est possible de prévoir un code spécifique Silverlight dans un source et sa variante WPF à la fois sans risque de mélange. Un seul code source existe ce qui évite la double maintenance.

Sous C# la compilation conditionnelle commence par un marqueur #if, de même nature que #region par exemple. Sauf que ce marqueur (ou “directive”) est suivi de la condition à tester (ou “pragma”). On peut écrire simplement :

#if Silverlight
    ...code spécifique SL
#endif
 
#if DEBUG
   Console.WriteLine("On est en debug!");
#endif

L’utilisation de #if peut intervenir n’importe où, tant que le code reste “compilable”. #if agit comme un “masque” qui supprime ou ajoute du code. Selon les mots clé définis Visual Studio grise dans l’éditeur le code qui n’est pas actif ce qui permet de voir facilement ce qui sera compilé ou non. Les aides à l’écriture du code comme IntelliSense, les messages d’erreur sous éditeur, etc, tout cela prend en compte le code à exécuter et gomme le code grisé comme s’il n’existait pas.

Le #if s’utilise aussi avec d’autre directives :

#endif qui termine le bloc #if
#define et #undef pour définir ou supprimer la définition d’un mot clé;
#else pour écrire un code alternatif si la condition échoue;
#elif, très peu connu, qui se comporte comme un #else #if en cascade.

Les conditions peuvent utiliser les opérateurs == (égalité), != (inégalité), && (et), || (ou). Les parenthèses sont aussi acceptées.

Tout code jugé non actif au moment de la compilation (ce qui dépend des mots clé définis) est tout simplement ignoré et non incorporé au binaire final.

Les problèmes posés par #if

Ils ne sont pas rédhibitoires mais ils existent.

Le premier et certainement le plus grave est l’atteinte à la lisibilité du code source.
les directives comme #if sont alignées collées à gauche or le code suit généralement des règles de mise en page tabulées. Les parties grisées s’insèrent alors dans des parties utiles, l’alignement visuel est perturbé, etc. Ponctuellement cela n’est pas gênant, mais dès qu’on utilise beaucoup la directive #if le code est plus difficile à lire.

Et la lecture du code source doit toujours être facilitée, même dans des cas où le code serait réputé plus académique ou plus rapide, un professionnel, un vrai (pas un frimeur qui étale sa science) choisira toujours la lisibilité. Car un logiciel professionnel se doit d’être maintenable à tout moment, même par des gens ne l’ayant pas écrit. C’est “le” critère peut-être premier d’un “bon code” (mais pas le seul !).

Donc tout ce qui altère la lisibilité doit être supprimé. Les directives #if, #else et consorts sont ainsi à fuir selon ce principe. Pourtant elles sont utiles... Heureusement il y a une alternative que nous verrons plus bas.

Autre problème plus factuel causé par l’utilisation de #if : la complexité de mise en œuvre.

Ne rigolez pas ! (enfin si, rire est bon pour la santé). Quand je parle de complexité de mise œuvre je ne parle bien entendu pas de celle du #if en lui-même... Mais qui dit code conditionnel, dit aussi appels à ce code conditionnel. Donc appels qui doivent eux-mêmes devenir conditionnels, sinon le code ne compile tout simplement pas ! Et là ça peut devenir le Bronx niveau lisibilité du source...

Imaginons le code suivant :

...
#if DEBUG
    private void sendDebugInfo(string message)
    { ... }
#endif
 
...
    string s = OperationA();
    sendDebugInfo(s);
    Operation(b)
...

Si nous compilons en mode Debug, tout ira bien. Mais si nous passons en mode Release, la compilation ne passera pas, “sendDebugInfo(s);” en plein milieu de notre code application est alors inconnu.

Il faut donc écrire :

...
#if DEBUG
    private void sendDebugInfo(string message)
    { ... }
#endif
 
...
    string s = OperationA();
#if DEBUG
    sendDebugInfo(s);
#endif
    Operation(b)
...

Et là tout de suite, si on colle plein d’appel de ce genre dans son code, c’est ce que j’appelais le Bronx plus haut... Ca devient illisible, en dehors d’être pénible à écrire.

Conclusion : le #if c’est super, ça marche, mais c’est un truc vieux comme le C et certainement avant et on ne peut vraiment pas dire que ces langages étaient réputés pour leur lisibilité...

Faire autrement s’impose. Heureusement le Framework apporte une solution bien plus élégante.

L’attribut Conditional

L’attribut “Conditional” (ou la classe ConditionalAttribute) permet de marquer une méthode ou une classe. Le code ainsi marqué est “conditionnel” dans le sens ou l’attribut prend en paramètre les mêmes pragmas que #if (par exemple “Debug” ou “Silverlight”).

Les différences essentielles avec #if

La première est que l’attribut se pose sur une classe ou une méthode. On ne le met plus n’importe où. Cela clarifie déjà un peu les choses. Il n’y a pas de bouts de code conditionnels.

La seconde découle de la première : c’est mille fois plus lisible et cela s’intègre parfaitement à la mise en page globale du code source.

La troisième est qu’il y a une part de magie derrière cet attribut : si je marque une méthode [Conditional(“DEBUG”], tout comme si elle était entourée d’un #if DEBUG elle ne sera plus compilée, mais surtout : tous les appels à cette méthode disparaitront du code final, ce qui règle l’un des problèmes essentiels de #if expliqué plus haut.

Pour être exact le code conditionnel n’est pas supprimé du binaire final, un test avec Reflector par exemple vous le prouvera facilement. Mais il n’est pas envoyé au JIT à l’exécution et l’ensemble des appels à ce code a bien disparu. En ce sens #if est donc plus “efficace” question “ménage” dans le binaire mais c’est très relatif.

Utilisation

L’exemple de code précédent devient ainsi :

...
    [Conditional("DEBUG"]
    private void sendDebugInfo(string message);
    { ... }
 
...
    string s = OperationA();
    sendDebugInfo(s);
    OperationB();
...

C’est beaucoup plus clair, plus concis et le code de l’application n’est pas perturbé par des directives #if. Il l’est déjà par l’instrumentalisation (l’appel à sendDebugInfo()), c’est déjà bien assez comme cela...

L’attribut Conditional peut être multiplié pour tester sur plusieurs conditions. Ce n’est pas comme #if qui accepte une expression (simple) qui sera évaluée. Dans le cas de l’attribut Conditional, si on veut tester deux conditions, il suffit de mettre deux fois l’attribut, chacun avec son test.

Conclusion

la directive #if rend le code moins lisible et moins maintenable mais elle fait ce qu’elle dit, totalement : si la condition ne s’évalue pas à True, tout le code marqué disparait du binaire final. Si on insère des blocs de codes énormes qu’on gère avec des #if (en se rappelant qu’on peut tester des tas de pragmas et pas seulement Debug!) on obtiendra un compilé plus léger. Dans ce cas précis #if prend l’avantage.

Dans tous les autres cas l’attribut Conditional est plus efficace, moins verbeux (les appels aux méthodes conditionnelles ne doivent pas être entourées de #if), plus lisible, et parfois souvent équivalent question taille de l’exe final (le code conditionnel de quelques lignes reste dans l’exe, mais s’il y a 100 appels, ils seront supprimés sans avoir rien de plus à écrire; le ratio final est proche de zéro).

Il ne s’agit pas de haute technologie du futur, mais discuter des petites choses simples qui rendent le code plus lisible et plus maintenable est tout aussi important !

Stay Tuned !

Faites des heureux, PARTAGEZ l'article !