Directives de préprocesseur C#

Bien que le compilateur n’ait pas de préprocesseur distinct, les directives décrites dans cette section sont traitées comme si c’était le cas. Vous les utilisez pour faciliter la compilation conditionnelle. Contrairement aux directives C et C++, vous ne pouvez pas utiliser ces directives pour créer des macros. Une directive de préprocesseur doit être la seule instruction sur une ligne.

Contexte pouvant accepter la valeur Null

La directive de préprocesseur #nullable définit le contexte d’annotation pouvant accepter la valeur Null et le contexte d’avertissement pouvant accepter la valeur Null. Cette directive contrôle si les annotations pouvant accepter la valeur Null ont un effet et si des avertissements d’acceptation de la valeur Null sont donnés. Chaque contexte est désactivé ou activé.

Les deux contextes peuvent être spécifiés au niveau du projet (en dehors du code source C#) en ajoutant l’élément Nullable à l’élément PropertyGroup. La directive #nullable contrôle les contextes d’annotation et d’avertissement et elle est prioritaire sur les paramètres au niveau du projet. Une directive définit le ou les contextes qu’elle contrôle jusqu’à ce qu’une autre directive la remplace, ou jusqu’à la fin du fichier source.

L’effet des directives est le suivant :

  • #nullable disable : définit les contextes d’avertissement et d’annotation pouvant accepter la valeur Null à désactivés.
  • #nullable enable : définit les contextes d’avertissement et d’annotation pouvant accepter la valeur Null sur activés.
  • #nullable restore : restaure les contextes d’avertissement et d’annotation pouvant accepter la valeur Null dans les paramètres du projet.
  • #nullable disable annotations : définit le contexte d’annotation pouvant accepter la valeur Null sur désactivé.
  • #nullable enable annotations : définit le contexte d’annotation pouvant accepter la valeur Null sur activé.
  • #nullable restore annotations : restaure le contexte d’annotation pouvant accepter la valeur Null dans les paramètres du projet.
  • #nullable disable warnings : définit le contexte d’avertissement nullable sur désactivé.
  • #nullable enable warnings : définit le contexte d’avertissement nullable sur activé.
  • #nullable restore warnings : restaure le contexte d’avertissement nullable avec les paramètres du projet.

Compilation conditionnelle

Vous utilisez quatre directives de préprocesseur pour contrôler la compilation conditionnelle :

  • #if : ouvre une compilation conditionnelle, où le code est compilé uniquement si le symbole spécifié est défini.
  • #elif : ferme la compilation conditionnelle précédente et ouvre une nouvelle compilation conditionnelle en fonction de la définition du symbole spécifié.
  • #else : ferme la compilation conditionnelle précédente et ouvre une nouvelle compilation conditionnelle si le symbole spécifié précédent n’est pas défini.
  • #endif : ferme la compilation conditionnelle précédente.

Le compilateur C# compile le code entre la directive #if et la directive #endif uniquement si le symbole spécifié est défini, ou non défini lorsque l’opérateur Not ! est utilisé. Contrairement à C et C++, vous ne pouvez pas attribuer de valeur numérique à un symbole. L’instruction #if en C# est booléenne et teste uniquement si le symbole a été défini ou non. Par exemple, le code suivant est compilé quand DEBUG est défini :

#if DEBUG
    Console.WriteLine("Debug version");
#endif

Le code suivant est compilé quand MYTEST n’est pas défini :

#if !MYTEST
    Console.WriteLine("MYTEST is not defined");
#endif

Vous pouvez utiliser les opérateurs == (égalité) et != (inégalité) pour tester les valeurs booltrue ou false. true signifie que le symbole est défini. L’instruction #if DEBUG a la même signification que #if (DEBUG == true). Vous pouvez utiliser les opérateurs && (et), || (ou), et ! (non) pour déterminer si plusieurs symboles ont été définis. Vous pouvez également regrouper des symboles et des opérateurs à l’aide de parenthèses.

Voici une directive complexe qui permet à votre code de tirer parti des fonctionnalités .NET plus récentes tout en restant rétrocompatible. Par exemple, imaginez que vous utilisez un package NuGet dans votre code, mais que le package prend uniquement en charge .NET 6 et versions ultérieures, ainsi que .NET Standard 2.0 et versions ultérieures :

#if (NET6_0_OR_GREATER || NETSTANDARD2_0_OR_GREATER)
    Console.WriteLine("Using .NET 6+ or .NET Standard 2+ code.");
#else
    Console.WriteLine("Using older code that doesn't support the above .NET versions.");
#endif

#if, ainsi que les directives #else, #elif, #endif, #define et #undef, vous permettent d’inclure ou d’exclure du code en fonction de l’existence d’un ou plusieurs symboles. La compilation conditionnelle peut être utile lors de la compilation du code pour une version Debug ou lors de la compilation d’une configuration spécifique.

Une directive conditionnelle commençant par une directive #if doit se terminer explicitement par une directive #endif. #define vous permet de définir un symbole. En utilisant ce dernier comme expression passée à la directive #if, l’expression correspond à true. Vous pouvez également définir un symbole avec l’option de compilateur DefineConstants. Vous pouvez annuler la définition d’un symbole avec #undef. La portée d’un symbole créé avec #define est le fichier dans lequel il a été défini. Un symbole que vous définissez avec DefineConstants ou #define n’est pas en conflit avec une variable du même nom. Autrement dit, un nom de variable ne doit pas être passé à une directive de préprocesseur, et un symbole ne peut être évalué que par une directive de préprocesseur.

#elif vous permet de créer une directive conditionnelle composée. L’expression #elif est évaluée si ni l’expression #if précédente ni une expression de directive #elif facultative précédente n’est évaluée à true. Si une expression #elif est évaluée à true, le compilateur évalue l’ensemble du code situé entre #elif et la directive conditionnelle suivante. Par exemple :

#define VC7
//...
#if DEBUG
    Console.WriteLine("Debug build");
#elif VC7
    Console.WriteLine("Visual Studio 7");
#endif

#else vous permet de créer une directive conditionnelle composée de sorte que, si aucune des expressions contenues dans les précédentes directives #if ou #elif (facultative) n’a la valeur true, le compilateur évalue tout le code entre #else et la directive #endif suivante. #endif(#endif) doit être la directive de préprocesseur suivante après #else.

#endif spécifie la fin d’une directive conditionnelle, qui a commencé avec la directive #if.

Le système de génération tient également compte des symboles de préprocesseur prédéfinis représentant différents frameworks cibles dans les projets de type SDK. Ils sont utiles durant la création d’applications pouvant cibler plusieurs versions de .NET.

Versions cibles de .NET Framework symboles Symboles supplémentaires
(disponibles dans les SDK .NET 5+)
Symboles de plateforme (disponibles uniquement
lorsque vous spécifiez un moniker de framework cible spécifique au système d’exploitation)
.NET Framework NETFRAMEWORK, NET481, NET48, NET472, NET471, NET47, NET462, NET461, NET46, NET452, NET451, NET45, NET40, NET35, NET20 NET48_OR_GREATER, NET472_OR_GREATER, NET471_OR_GREATER, NET47_OR_GREATER, NET462_OR_GREATER, NET461_OR_GREATER, NET46_OR_GREATER, NET452_OR_GREATER, NET451_OR_GREATER, NET45_OR_GREATER, NET40_OR_GREATER, NET35_OR_GREATER, NET20_OR_GREATER
.NET Standard NETSTANDARD, NETSTANDARD2_1, NETSTANDARD2_0, NETSTANDARD1_6, NETSTANDARD1_5, NETSTANDARD1_4, NETSTANDARD1_3, NETSTANDARD1_2, NETSTANDARD1_1, NETSTANDARD1_0 NETSTANDARD2_1_OR_GREATER, NETSTANDARD2_0_OR_GREATER, NETSTANDARD1_6_OR_GREATER, NETSTANDARD1_5_OR_GREATER, NETSTANDARD1_4_OR_GREATER, NETSTANDARD1_3_OR_GREATER, NETSTANDARD1_2_OR_GREATER, NETSTANDARD1_1_OR_GREATER, NETSTANDARD1_0_OR_GREATER
.NET 5+ (et .NET Core) NET, NET9_0, NET8_0, NET7_0, NET6_0, NET5_0, NETCOREAPP, NETCOREAPP3_1, NETCOREAPP3_0, NETCOREAPP2_2, NETCOREAPP2_1, NETCOREAPP2_0, NETCOREAPP1_1, NETCOREAPP1_0 NET8_0_OR_GREATER, NET7_0_OR_GREATER, NET6_0_OR_GREATER, NET5_0_OR_GREATER, NETCOREAPP3_1_OR_GREATER, NETCOREAPP3_0_OR_GREATER, NETCOREAPP2_2_OR_GREATER, NETCOREAPP2_1_OR_GREATER, NETCOREAPP2_0_OR_GREATER, NETCOREAPP1_1_OR_GREATER, NETCOREAPP1_0_OR_GREATER ANDROID, BROWSER, IOS, MACCATALYST, MACOS, TVOS, WINDOWS,
[OS][version] (par exemple, IOS15_1),
[OS][version]_OR_GREATER (par exemple IOS15_1_OR_GREATER)

Notes

  • Les symboles sans version sont définis indépendamment de la version que vous ciblez.
  • Les symboles spécifiques à la version sont définis uniquement pour la version que vous ciblez.
  • Les symboles <framework>_OR_GREATER sont définis pour la version que vous ciblez et toutes les versions antérieures. Par exemple, si vous ciblez .NET Framework 2.0, les symboles suivants sont définis : NET20, NET20_OR_GREATER, NET11_OR_GREATER et NET10_OR_GREATER.
  • Les symboles NETSTANDARD<x>_<y>_OR_GREATER sont définis uniquement pour les cibles .NET Standard, et non pour les cibles qui implémentent .NET Standard, telles que .NET Core et .NET Framework.
  • Ils sont différents des monikers de framework cible utilisés par la propriété MSBuild TargetFramework et NuGet.

Notes

Pour les projets non-SDK traditionnels, vous devez configurer manuellement les symboles de compilation conditionnelle pour les différents frameworks cibles dans Visual Studio via les pages de propriétés du projet.

D’autres symboles prédéfinis incluent les constantes DEBUG et TRACE. Vous pouvez remplacer les valeurs définies pour le projet à l’aide de #define. Le symbole DEBUG, par exemple, est automatiquement défini en fonction de vos propriétés de configuration de build (mode « Debug » ou « Release »).

L’exemple suivant montre comment définir un symbole MYTEST sur un fichier, puis tester les valeurs des symboles MYTEST et DEBUG. La sortie de cet exemple dépend du mode de configuration que vous avez utilisé pour créer le projet (Debug ou Release).

#define MYTEST
using System;
public class MyClass
{
    static void Main()
    {
#if (DEBUG && !MYTEST)
        Console.WriteLine("DEBUG is defined");
#elif (!DEBUG && MYTEST)
        Console.WriteLine("MYTEST is defined");
#elif (DEBUG && MYTEST)
        Console.WriteLine("DEBUG and MYTEST are defined");
#else
        Console.WriteLine("DEBUG and MYTEST are not defined");
#endif
    }
}

L’exemple suivant montre comment tester différents frameworks cibles pour pouvoir utiliser les nouvelles API dans la mesure du possible :

public class MyClass
{
    static void Main()
    {
#if NET40
        WebClient _client = new WebClient();
#else
        HttpClient _client = new HttpClient();
#endif
    }
    //...
}

Définition de symboles

Vous utilisez les deux directives de préprocesseur suivantes pour définir ou annuler la définition de symboles pour la compilation conditionnelle :

  • #define : permet de définir un symbole.
  • #undef : permet d’annuler la définition d’un symbole.

Vous utilisez #define pour définir un symbole. Quand vous utilisez le symbole sous forme d’expression passée à la directive #if, l’expression donne la valeur true, comme illustré dans l’exemple suivant :

#define VERBOSE

#if VERBOSE
   Console.WriteLine("Verbose output version");
#endif

Remarque

En C#, les constantes primitives doivent être définies à l’aide du mot clé const. Une déclaration const crée un membre static qui ne peut pas être modifié au moment de l’exécution. La directive #define ne peut pas être utilisée pour déclarer des valeurs constantes comme c’est le cas habituellement en C et en C++. Si vous avez plusieurs constantes de ce type, créez une classe « Constantes » distincte pour les stocker.

Les symboles peuvent être utilisés pour spécifier des conditions de compilation. Vous pouvez tester le symbole avec #if ou #elif. Vous pouvez également utiliser ConditionalAttribute pour effectuer une compilation conditionnelle. Vous pouvez définir un symbole, mais vous ne pouvez pas attribuer de valeur à un symbole. La directive #define doit apparaître dans le fichier avant l’utilisation d’instructions qui ne sont pas également des directives de préprocesseur. Vous pouvez également définir un symbole avec l’option de compilateur DefineConstants. Vous pouvez annuler la définition d’un symbole avec #undef.

Définition de régions

Vous pouvez définir des régions de code qui peuvent être réduites dans un plan à l’aide des deux directives de préprocesseur suivantes :

  • #region : permet de démarrer une région.
  • #endregion : permet de mettre fin à une région.

#region vous permet de spécifier un bloc de code que vous pouvez développer ou réduire quand vous utilisez la fonctionnalité Mode Plan de l’éditeur de code. Dans les fichiers de code volumineux, il peut être pratique de réduire ou masquer une ou plusieurs régions pour vous concentrer sur la partie du fichier sur laquelle vous êtes en train de travailler. L’exemple suivant montre comment définir une région :

#region MyClass definition
public class MyClass
{
    static void Main()
    {
    }
}
#endregion

Un bloc #region doit se terminer par une directive #endregion. Un bloc #region ne peut pas chevaucher un bloc #if. Toutefois, un bloc #region peut être imbriqué dans un bloc #if et, inversement, un bloc #if peut être imbriqué dans un bloc #region.

Informations d’erreur et d’avertissement

Vous demandez au compilateur de générer des erreurs et des avertissements du compilateur définis par l’utilisateur, ainsi que des informations de ligne de contrôle à l’aide des directives suivantes :

  • #error : permet de générer une erreur du compilateur avec un message spécifié.
  • #warning : permet de générer un avertissement du compilateur, avec un message spécifique.
  • #line : permet de modifier le numéro de ligne imprimé avec les messages du compilateur.

#error vous permet de générer une erreur définie par l’utilisateur CS1029 à partir d’un emplacement spécifique dans votre code. Par exemple :

#error Deprecated code in this method.

Notes

Le compilateur traite #error version de manière spéciale et signale une erreur de compilateur CS8304, avec un message contenant les versions du compilateur et du langage utilisés.

#warning vous permet de générer un avertissement du compilateur de premier niveau CS1030 à partir d’un emplacement spécifique dans votre code. Par exemple :

#warning Deprecated code in this method.

#line vous permet de changer la numérotation de lignes du compilateur et (éventuellement) le nom de fichier pour les erreurs et les avertissements.

L’exemple suivant montre comment signaler deux avertissements associés à des numéros de ligne. La directive #line 200 assigne de force le numéro 200 à la ligne suivante (bien que le numéro par défaut soit 6) ; jusqu’à la prochaine directive #line, le nom de fichier indiqué est « Special ». La directive #line default rétablit la numérotation de lignes par défaut, qui compte les lignes renumérotées par la directive précédente.

class MainClass
{
    static void Main()
    {
#line 200 "Special"
        int i;
        int j;
#line default
        char c;
        float f;
#line hidden // numbering not affected
        string s;
        double d;
    }
}

La compilation produit la sortie suivante :

Special(200,13): warning CS0168: The variable 'i' is declared but never used
Special(201,13): warning CS0168: The variable 'j' is declared but never used
MainClass.cs(9,14): warning CS0168: The variable 'c' is declared but never used
MainClass.cs(10,15): warning CS0168: The variable 'f' is declared but never used
MainClass.cs(12,16): warning CS0168: The variable 's' is declared but never used
MainClass.cs(13,16): warning CS0168: The variable 'd' is declared but never used

La directive #line peut être utilisée dans une étape intermédiaire automatisée du processus de génération. Par exemple, si des lignes ont été supprimées du fichier de code source d’origine, mais que vous voulez que le compilateur continue de générer une sortie sur la base de la numérotation de lignes d’origine du fichier, vous pouvez supprimer les lignes et simuler ensuite la numérotation de lignes d’origine avec #line.

La directive #line hidden masque les lignes successives du débogueur, de sorte que quand le développeur parcourt le code, les lignes situées entre une directive #line hidden et la directive #line suivante (en supposant qu’il ne s’agit pas d’une autre directive #line hidden) sont ignorées. Cette option peut aussi être utilisée pour permettre à ASP.NET de différencier le code défini par l’utilisateur du code généré par l’ordinateur. Même si ASP.NET est le principal utilisateur de cette fonctionnalité, il est probable que d’autres générateurs de code source pourront l’utiliser.

Une directive #line hidden n’affecte ni les noms de fichiers ni les numéros de lignes dans les rapports d’erreurs. Autrement dit, si le compilateur repère une erreur dans un bloc masqué, il indique le nom du fichier et le numéro de ligne actuels de l’erreur.

La directive #line filename spécifie le nom de fichier que vous souhaitez voir apparaître dans la sortie du compilateur. Par défaut, le nom réel du fichier de code source est utilisé. Le nom de fichier doit être entre guillemets doubles (" ") et doit être précédé d’un numéro de ligne.

À compter de C# 10, vous pouvez utiliser une nouvelle forme de directive #line :

#line (1, 1) - (5, 60) 10 "partial-class.cs"
/*34567*/int b = 0;

Les composants de cette forme sont les suivants :

  • (1, 1) : ligne de début et colonne pour le premier caractère de la ligne qui suit la directive. Dans cet exemple, la ligne suivante est signalée comme ligne 1, colonne 1.
  • (5, 60) : ligne de fin et colonne pour la région marquée.
  • 10 : décalage de colonne pour que la directive #line prenne effet. Dans cet exemple, la 10e colonne serait signalée comme colonne 1. C’est là que commence la déclaration int b = 0;. Ce champ est facultatif. En cas d’omission, la directive prend effet sur la première colonne.
  • "partial-class.cs" : nom du fichier de sortie.

L’exemple précédent génère l’avertissement suivant :

partial-class.cs(1,5,1,6): warning CS0219: The variable 'b' is assigned but its value is never used

Après le remappage, la variable b se trouve sur la première ligne, au caractère six, du fichier partial-class.cs.

Les langages spécifiques à un domaine utilisent généralement ce format pour fournir un meilleur mappage du fichier source à la sortie C# générée. L’utilisation la plus courante de cette directive #line étendue consiste à mapper des avertissements ou des erreurs qui apparaissent dans un fichier généré à la source d’origine. Prenons comme exemple cette page Razor :

@page "/"
Time: @DateTime.NowAndThen

La propriété DateTime.Now a été tapée de manière incorrecte en tant que DateTime.NowAndThen. Le code C# généré pour cet extrait de code Razor ressemble à ce qui suit, dans page.g.cs :

  _builder.Add("Time: ");
#line (2, 6) - (2, 27) 15 "page.razor"
  _builder.Add(DateTime.NowAndThen);

La sortie du compilateur pour l’extrait de code précédent est la suivante :

page.razor(2, 2, 2, 27)error CS0117: 'DateTime' does not contain a definition for 'NowAndThen'

La ligne 2, colonne 6 dans page.razor est l’endroit où commence le texte @DateTime.NowAndThen. Voici ce qui est indiqué dans la directive : (2, 6). Cette étendue de @DateTime.NowAndThen se termine à la ligne 2, colonne 27. Voici ce qui est indiqué dans la directive : (2, 27). Le texte pour DateTime.NowAndThen commence dans la colonne 15 de page.g.cs. Voici ce qui est indiqué dans la directive : 15. En plaçant tous les arguments ensemble, le compilateur signale l’erreur à son emplacement dans page.razor. Le développeur peut accéder directement à l’erreur dans son code source, et non dans la source générée.

Pour afficher d’autres exemples de ce format, consultez la spécification de fonctionnalité dans la section sur les exemples.

Pragmas

La directive #pragma fournit au compilateur des instructions spéciales pour la compilation du fichier dans lequel elle apparaît. Les instructions doivent être prises en charge par le compilateur. En d’autres termes, vous ne pouvez pas utiliser #pragma pour créer des instructions de prétraitement personnalisées.

#pragma pragma-name pragma-arguments

pragma-name est le nom d’un pragma reconnu et pragma-arguments correspond aux arguments spécifiques au pragma.

#pragma warning

#pragma warning peut activer ou désactiver certains avertissements.

#pragma warning disable warning-list
#pragma warning restore warning-list

warning-list est une liste séparée par des virgules de numéros d’avertissement. Le préfixe « CS » est facultatif. Quand aucun numéro d’avertissement n’est spécifié, disable désactive tous les avertissements et restore active tous les avertissements.

Notes

Pour trouver les numéros d’avertissement dans Visual Studio, générez votre projet, puis recherchez les numéros d’avertissement dans la fenêtre Sortie.

disable prend effet à partir de la ligne suivante du fichier source. L’avertissement est restauré sur la ligne suivant l’élément restore. En l’absence d’élément restore dans le fichier, les avertissements sont restaurés à leur état par défaut à la première ligne de tous les fichiers ultérieurs de la même compilation.

// pragma_warning.cs
using System;

#pragma warning disable 414, CS3021
[CLSCompliant(false)]
public class C
{
    int i = 1;
    static void Main()
    {
    }
}
#pragma warning restore CS3021
[CLSCompliant(false)]  // CS3021
public class D
{
    int i = 1;
    public static void F()
    {
    }
}

checksum #pragma

Génère des sommes de contrôle pour les fichiers sources afin de faciliter le débogage des pages ASP.NET.

#pragma checksum "filename" "{guid}" "checksum bytes"

"filename" est le nom du fichier qui nécessite la surveillance des modifications ou des mises à jour, "{guid}" est l’identificateur global unique (GUID) de l’algorithme de hachage, et "checksum_bytes" est la chaîne de chiffres hexadécimaux représentant les octets de la somme de contrôle. Doit être un nombre pair de chiffres hexadécimaux. S’il y a un nombre impair de chiffres, un avertissement est généré au moment de la compilation et la directive est ignorée.

Le débogueur Visual Studio utilise une somme de contrôle pour s’assurer de toujours trouver la bonne source. Le compilateur calcule la somme de contrôle pour un fichier source, puis envoie la sortie vers le fichier de base de données du programme (PDB). Le débogueur utilise ensuite le fichier PDB à comparer avec la somme de contrôle qu’il calcule pour le fichier source.

Cette solution ne fonctionne pas pour les projets ASP.NET, car la somme de contrôle est calculée pour le fichier source généré, au lieu du fichier .aspx. Pour résoudre ce problème, #pragma checksum fournit une prise en charge de la somme de contrôle pour les pages ASP.NET.

Quand vous créez un projet ASP.NET dans Visual C#, le fichier source généré contient une somme de contrôle pour le fichier .aspx, à partir duquel la source est générée. Le compilateur écrit ensuite ces informations dans le fichier PDB.

Si le compilateur ne rencontre aucune directive #pragma checksum dans le fichier, il calcule la somme de contrôle et écrit la valeur dans le fichier PDB.

class TestClass
{
    static int Main()
    {
        #pragma checksum "file.cs" "{406EA660-64CF-4C82-B6F0-42D48172A799}" "ab007f1d23d9" // New checksum
    }
}