Direttive per il preprocessore C#

Anche se il compilatore non ha un preprocessore indipendente, le direttive descritte in questa sezione vengono elaborate come se ne esistesse uno. È possibile usarli per facilitare la compilazione condizionale. A differenza delle direttive di C e C++, non è possibile usare queste direttive per creare macro. Una direttiva del preprocessore deve essere l'unica istruzione su una riga.

Contesto nullable

La direttiva del preprocessore #nullable imposta il contesto di annotazione nullable e ilcontesto di avviso nullable. Questa direttiva controlla se le annotazioni nullable hanno effetto e se vengono visualizzati avvisi di nullbility. Ogni contesto è disabilitato o abilitato.

Entrambi i contesti possono essere specificati a livello di progetto (all'esterno del codice sorgente C#) aggiungendo l'elemento Nullable all'elemento PropertyGroup. La direttiva #nullable controlla i contesti di annotazione e avviso e ha la precedenza sulle impostazioni a livello di progetto. Una direttiva imposta i contesti che controlla fino a quando un'altra direttiva non esegue l'override o fino alla fine del file di origine.

L'effetto delle direttive è il seguente:

  • #nullable disable: imposta le annotazioni nullable e i contesti di avviso su disabilitati.
  • #nullable enable: imposta i contesti di annotazione e avviso nullable su abilitato.
  • #nullable restore: ripristina i contesti di annotazione e avviso nullable nelle impostazioni del progetto.
  • #nullable disable annotations: imposta il contesto di annotazione nullable su disabilitato.
  • #nullable enable annotations: imposta il contesto di annotazione nullable su abilitato.
  • #nullable restore annotations: ripristina il contesto dell'annotazione nullable nelle impostazioni del progetto.
  • #nullable disable warnings: imposta il contesto di avviso nullable su disabilitato.
  • #nullable enable warnings: imposta il contesto di avviso nullable su abilitato.
  • #nullable restore warnings: ripristina il contesto di avviso nullable nelle impostazioni del progetto.

Compilazione condizionale

Per controllare la compilazione condizionale si usano quattro direttive del preprocessore:

  • #if: apre una compilazione condizionale, in cui il codice viene compilato solo se è definito il simbolo specificato.
  • #elif: chiude la compilazione condizionale precedente e apre una nuova compilazione condizionale in base a se è definito il simbolo specificato.
  • #else: chiude la compilazione condizionale precedente e apre una nuova compilazione condizionale se il simbolo specificato precedente non è definito.
  • #endif: chiude la compilazione condizionale precedente.

Il compilatore C# compila il codice tra la direttiva #if e la direttiva #endif solo se il simbolo specificato è definito o non definito quando viene usato l'operatore not !. A differenza di C e C++, non è possibile assegnare un valore numerico a un simbolo. L'istruzione #if in C# è booleana e verifica solo se il simbolo è stato definito o meno. Ad esempio, il codice seguente viene compilato quando DEBUG viene definito:

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

Il codice seguente viene compilato quando MYTEST non viene definito:

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

È possibile usare gli operatori == (uguaglianza) e != (disuguaglianza) per testare i valoribool true o false. true indica che il simbolo è definito. L'istruzione #if DEBUG ha lo stesso significato di #if (DEBUG == true). È possibile usare gli operatori && (e), || (o)e ! (non) per valutare se sono stati definiti più simboli. È anche possibile raggruppare simboli e operatori tra parentesi.

Di seguito è riportata una direttiva complessa che consente al codice di sfruttare le funzionalità .NET più recenti pur rimanendo compatibile con le versioni precedenti. A esempio, si supponga di usare un pacchetto NuGet nel codice, ma il pacchetto supporta solo .NET 6 e versioni successive, oltre a .NET Standard 2.0 e versioni successive:

#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, insieme alle direttive #else, #elif, #endif, #define e #undef, consente di includere o escludere il codice in base all'esistenza di uno o più simboli. La compilazione condizionale può essere utile quando si compila il codice per una compilazione di debug o durante la compilazione per una configurazione specifica.

Una direttiva condizionale che inizia con una direttiva #if deve essere terminata in modo esplicito con una direttiva #endif. #define consente di definire un simbolo. Usando poi il simbolo come espressione passata alla direttiva #if, l'espressione restituisce true. È anche possibile definire un simbolo con l'opzione del compilatore DefineConstants. È possibile annullare la definizione di un simbolo con #undef. L'ambito di un simbolo creato con #define è il file in cui è stato definito. Un simbolo definito con DefineConstants o con #define non è in conflitto con una variabile con lo stesso nome. Ovvero, un nome di variabile non deve essere passato a una direttiva del preprocessore e un simbolo può essere valutato solo da una direttiva del preprocessore.

#elif consente di creare una direttiva condizionale composita. L'espressione #elif verrà valutata se né l'espressione #if precedente né le espressioni di direttiva precedenti, #elif, facoltative, restituiscono true. Se un'espressione #elif restituisce true, il compilatore valuterà tutto il codice compreso tra #elif e la direttiva condizionale successiva. Ad esempio:

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

#else consente di creare una direttiva condizionale composta, in modo che, se nessuna delle espressioni nelle direttive #if precedente o #elif (facoltativa) restituisce true, il compilatore valuterà tutto il codice tra #else e il #endif successivo. #endif(#endif) deve essere la direttiva del preprocessore successiva dopo #else.

#endif specifica la fine di una direttiva condizionale, iniziata con la direttiva #if.

Il sistema di compilazione riconosce anche i simboli predefiniti del preprocessore che rappresentano framework di destinazione diversi nei progetti in stile SDK. Sono utili per la creazione di applicazioni destinate a più versioni di .NET.

Framework di destinazione Simboli Simboli aggiuntivi
(disponibile in .NET 5+ SDK)
Simboli della piattaforma (disponibile solo
quando si specifica un TFM specifico del sistema operativo)
.NET Framework NETFRAMEWORK, 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+ (e .NET Core) NET, 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] (ad esempio IOS15_1),
[OS][version]_OR_GREATER (ad esempio, IOS15_1_OR_GREATER)

Nota

  • I simboli senza versione vengono definiti indipendentemente dalla versione di destinazione.
  • I simboli specifici della versione sono definiti solo per la versione di destinazione.
  • I simboli <framework>_OR_GREATER sono definiti per la versione di destinazione e per tutte le versioni precedenti. Ad esempio, se si ha come destinazione .NET Framework 2.0, vengono definiti i simboli seguenti: NET20, NET20_OR_GREATER, NET11_OR_GREATER e NET10_OR_GREATER.
  • I simboli NETSTANDARD<x>_<y>_OR_GREATER sono definiti solo per le destinazioni .NET Standard e non per le destinazioni che implementano .NET Standard, ad esempio .NET Core e .NET Framework.
  • Questi sono diversi dai moniker del framework di destinazione (TFMs) usati dalla proprietà TargetFramework MSBuild e Da NuGet.

Nota

Per i progetti tradizionali non in stile SDK, è necessario configurare manualmente i simboli di compilazione condizionale per i diversi framework di destinazione in Visual Studio tramite le pagine delle proprietà del progetto.

Altri simboli predefiniti includono le costanti DEBUG e TRACE. È possibile sostituire i valori impostati per il progetto con #define. Il simbolo DEBUG, ad esempio, viene impostato automaticamente a seconda delle proprietà di configurazione della build (modalità "Debug" o "Versione").

Nell'esempio seguente viene illustrato come definire un simbolo di MYTEST in un file e quindi testare i valori dei simboli MYTEST e DEBUG. L'output di questo esempio dipende dal fatto che il progetto sia stato compilato in modalità di configurazione debug o versione.

#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'esempio seguente illustra come eseguire test per framework di destinazione diversi, in modo da poter usare le API più recenti quando possibile:

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

Definizione dei simboli

Usare le due direttive del preprocessore seguenti per definire o annullare la definizione dei simboli per la compilazione condizionale:

  • #define: definire un simbolo.
  • #undef: annullare la definizione di un simbolo.

Si usa #define per definire un simbolo. Quando si usa il simbolo come espressione passata alla direttiva #if, l'espressione restituirà true, come illustrato nell'esempio seguente:

#define VERBOSE

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

Nota

In C# le costanti primitive devono essere definite usando la parola chiave const. Una dichiarazione const crea un membro static che non può essere modificato in fase di esecuzione. Non è possibile usare la direttiva #define per dichiarare valori costanti come avviene in genere in C e in C++. Se sono presenti più costanti di questo tipo, per usarle può essere utile creare una classe di costanti separata.

Per specificare le condizioni per la compilazione è possibile usare simboli. È possibile testare del simbolo con #if o #elif. È anche possibile usare ConditionalAttribute per eseguire una compilazione condizionale. È possibile definire un simbolo, ma non è possibile assegnare un valore a un simbolo. La direttiva #define deve essere inserita in un file prima di usare istruzioni che non siano anche direttive del preprocessore. È anche possibile definire un simbolo con l'opzione del compilatore DefineConstants. È possibile annullare la definizione di un simbolo con #undef.

Definizione delle aree

È possibile definire aree di codice che possono essere compresse in una struttura usando le due direttive del preprocessore seguenti:

  • #region: avviare un'area.
  • #endregion: terminare un'area.

#region consente di specificare un blocco di codice che è possibile espandere o comprimere quando si usa la struttura funzionalità dell'editor di codice. Nei file di codice più lunghi è utile comprimere o nascondere una o più aree in modo che sia possibile concentrarsi sulla parte del file attualmente in uso. L'esempio seguente illustra come definire un'area:

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

Un blocco #region deve essere terminato con una direttiva #endregion. Un blocco #region non può sovrapporsi a un blocco #if. Tuttavia, è possibile annidare un blocco #region in un blocco #if e un blocco #if in un blocco #region.

Informazioni sull'errore e sull'avviso

Si indica al compilatore di generare errori e avvisi del compilatore definiti dall'utente e informazioni sulla riga di controllo usando le direttive seguenti:

  • #error: generare un errore del compilatore con un messaggio specificato.
  • #warning: generare un avviso del compilatore con un messaggio specifico.
  • #line: modificare il numero di riga stampato con i messaggi del compilatore.

#error consente di generare l'errore definito dall'utente CS1029 da una posizione specifica nel codice. Ad esempio:

#error Deprecated code in this method.

Nota

Il compilatore tratta #error version in modo speciale e segnala un errore del compilatore, CS8304, con un messaggio contenente il compilatore e le versioni del linguaggio usate.

#warning consente di generare l'avviso CS1030 di livello uno del compilatore da una posizione specifica del codice. Ad esempio:

#warning Deprecated code in this method.

#line consente di modificare il numero di riga del compilatore e, facoltativamente, l'output del nome del file per gli errori e gli avvisi.

Nell'esempio seguente viene illustrata la modalità di segnalazione di due avvisi associati a numeri di riga. La direttiva #line 200 forza l'impostazione del numero di riga successivo su 200 (anche se l'impostazione predefinita è 6) e, fino alla successiva direttiva #line, il nome file viene indicato come "Special". La direttiva #line default reimposta la numerazione predefinita delle righe, con il conteggio delle righe rinumerate dalla direttiva precedente.

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 compilazione produce l'output seguente:

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 direttiva #line può essere usata in un'istruzione automatizzata intermedia nel processo di compilazione. Se, ad esempio, sono state rimosse delle righe dal file del codice sorgente originale e si vuole che il compilatore generi comunque un output basato sulla numerazione originale delle righe del file, è possibile rimuovere le righe e simulare la numerazione originale tramite #line.

La direttiva #line hidden nasconde le righe successive dal debugger, in modo che, quando lo sviluppatore esegue il codice, tutte le righe tra una direttiva #line hidden e la successiva #line (presupponendo che non sia un'altra direttiva #line hidden) venga superata. Questa opzione può essere usata anche per consentire ad ASP.NET di distinguere il codice definito dall'utente da quello generato dal computer. Sebbene ASP.NET rappresenti il consumer principale di questa funzionalità, è probabile che ne usufruiscano anche altri generatori di codice sorgente.

Una direttiva #line hidden non influisce sui nomi di file o sui numeri di riga nella segnalazione degli errori. Cioè, se il compilatore rileva un errore in un blocco nascosto, il compilatore segnala il nome del file corrente e il numero di riga dell'errore.

La direttiva #line filename specifica il nome del file che si vuole venga visualizzato nell'output del compilatore. Per impostazione predefinita, viene usato il nome effettivo del file del codice sorgente. Il nome del file deve essere racchiuso tra virgolette doppie (" ") e deve essere preceduto da un numero di riga.

A partire da C# 10, è possibile usare una nuova forma della direttiva #line:

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

I componenti di questa forma sono:

  • (1, 1): riga iniziale e colonna per il primo carattere nella riga che segue la direttiva. In questo esempio, la riga successiva verrà segnalata come riga 1, colonna 1.
  • (5, 60): riga finale e colonna per l'area contrassegnata.
  • 10: offset di colonna per la direttiva #line da rendere effettiva. In questo esempio, la colonna 10 verrà segnalata come colonna 1. È qui che inizia la dichiarazione int b = 0;. Questo campo è facoltativo. Se omessa, la direttiva diventa effettiva sulla prima colonna.
  • "partial-class.cs": nome del file di output.

L'esempio precedente genera l'avviso seguente:

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

Dopo il mapping, la variabile, b, si trova nella prima riga, in corrispondenza del carattere sei, del file partial-class.cs.

I linguaggi specifici del dominio (DSLS) usano in genere questo formato per fornire un mapping migliore dal file di origine all'output C# generato. L'uso più comune di questa direttiva#line estesa consiste nell’eseguire nuovamente il mapping degli avvisi o degli errori visualizzati in un file generato nell'origine originale. Si consideri ad esempio questa pagina razor:

@page "/"
Time: @DateTime.NowAndThen

La proprietà DateTime.Now è stata digitata in modo non corretto come DateTime.NowAndThen. Il codice C# generato per questo frammento di codice razor è simile al seguente, in page.g.cs:

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

L'output del compilatore per il frammento di codice precedente è:

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

La riga 2, colonna 6 in page.razor è la posizione in cui inizia il testo @DateTime.NowAndThen. Questo è indicato nella direttiva (2, 6). Tale intervallo di @DateTime.NowAndThen termina alla riga 2, colonna 27. Questo è indicato dalla direttiva (2, 27). Il testo per DateTime.NowAndThen inizia nella colonna 15 di page.g.cs. Questo è indicato dalla direttiva 15. L'inserimento di tutti gli argomenti e il compilatore segnala l'errore nella relativa posizione in page.razor. Lo sviluppatore può passare direttamente all'errore nel codice sorgente, non all'origine generata.

Per altri esempi di questo formato, vedere la specifica della funzionalità nella sezione sugli esempi.

Pragma

#pragma fornisce al compilatore istruzioni speciali per la compilazione del file in cui si trova. Le istruzioni devono essere supportate dal compilatore. In altre parole, non è possibile usare #pragma per creare istruzioni di pre-elaborazione personalizzate.

#pragma pragma-name pragma-arguments

Dove pragma-name è il nome di un pragma riconosciuto e pragma-arguments è l'argomento specifico del pragma.

avviso #pragma

#pragma warning consente di abilitare o disabilitare alcuni avvisi.

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

Dove warning-list è un elenco delimitato da virgole di numeri di avviso. Il prefisso "CS" è facoltativo. Quando non viene specificato alcun numero di avviso, disable disabilita tutti gli avvisi e restore abilita tutti gli avvisi.

Nota

Per trovare i numeri di avviso in Visual Studio, compilare il progetto e quindi cercare i numeri di avviso nella finestra Output.

L'oggetto disable ha effetto a partire dalla riga successiva del file di origine. L'avviso viene ripristinato nella riga che segue restore. Se non è presente alcun restore nel file, gli avvisi vengono ripristinati nello stato predefinito alla prima riga di tutti i file successivi nella stessa compilazione.

// 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

Genera i checksum per i file di origine per favorire il debug delle pagine ASP.NET.

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

Dove "filename" è il nome del file che richiede il monitoraggio delle modifiche o degli aggiornamenti, "{guid}" è il GUID (Global Unique Identifier) per l'algoritmo hash ed "checksum_bytes" è la stringa di cifre esadecimali che rappresentano i byte del checksum. Deve essere un numero pari di cifre esadecimali. Un numero dispari di cifre genera un avviso in fase di compilazione e la direttiva viene ignorata.

Il debugger di Visual Studio usa un checksum per trovare sempre l'origine corretta. Il compilatore calcola il checksum di un file di origine, quindi genera l'output nel file del database di programma (PDB). Il PDB viene quindi usato dal debugger per eseguire il confronto con il checksum calcolato per il file di origine.

Questa soluzione non funziona per i progetti ASP.NET, perché il checksum calcolato riguarda il file di origine generato, anziché il file di .aspx. Per risolvere questo problema, #pragma checksum offre il supporto del checksum per le pagine ASP.NET.

Quando si crea un progetto ASP.NET in Visual C# il file di origine generato contiene un checksum per il file con estensione aspx, dal quale viene generata l'origine. Queste informazioni vengono quindi scritte dal compilatore nel file PDB.

Se il compilatore non trova una direttiva #pragma checksum nel file, calcola il checksum e scrive il valore nel file PDB.

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