Condividi tramite


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 le annotazioni e i flag di avvertimento nel contesto nullable . Questa direttiva controlla se le annotazioni nullable hanno effetto e se vengono visualizzati avvisi di nullbility. Ogni flag è 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 flag di annotazione e avviso e ha la precedenza sulle impostazioni a livello di progetto. Una direttiva imposta il flag 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 il contesto nullable disabilitato.
  • #nullable enable: imposta il contesto nullable abilitato.
  • #nullable restore: ripristina il contesto nullable nelle impostazioni del progetto.
  • #nullable disable annotations: imposta il flag delle annotazioni nel contesto nullable su disattivato.
  • #nullable enable annotations: imposta il flag di annotazioni nel contesto nullable su abilitato.
  • #nullable restore annotations: ripristina il flag di annotazioni nel contesto nullable alle impostazioni del progetto.
  • #nullable disable warnings: imposta il flag di avviso nel contesto a valori nulli su disabilitato .
  • #nullable enable warnings: imposta il flag di avviso nel contesto nullable su abilitato.
  • #nullable restore warnings: ripristina il flag di avviso nel contesto 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 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, 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+ (e .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] (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").

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 è 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 MYTESTnon viene definito:

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

È possibile usare gli operatori == (uguaglianza) e != (disuguaglianza) per testare i valoribooltrue 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 definiti più simboli. È anche possibile raggruppare simboli e operatori tra parentesi.

L'esempio seguente illustra una direttiva complessa che consente al codice di sfruttare le funzionalità .NET più recenti pur rimanendo compatibili 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.

#elif consente di creare una direttiva condizionale composita. L'espressione #elif viene valutata se né l'#if precedente né le espressioni di direttiva precedenti, facoltative #elif 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.

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 utilizza un simbolo come espressione passata alla direttiva #if, l'espressione restituisce 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 impone che il numero della riga successiva sia 200 (anche se il valore predefinito è #6) e fino alla successiva direttiva #line, il nome file verrà segnalato come "Speciale". La direttiva #line default restituisce la numerazione della riga alla numerazione predefinita, che conta le righe numerate 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. Anche se ASP.NET è il principale utente di questa funzionalità, è probabile che sempre più generatori di codice sorgente la utilizzino.

Una direttiva #line hidden non influisce sui nomi di file o sui numeri di riga nella segnalazione degli errori. Ovvero, 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 tra virgolette doppie ("") e deve seguire un numero di riga.

È 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. La dichiarazione int b = 0; inizia in corrispondenza di tale colonna. 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 estesa #line consiste nel rimappare gli avvisi o gli errori visualizzati in un file generato alla sorgente 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, indicato da (2, 6) nella direttiva . Tale intervallo di @DateTime.NowAndThen termina alla riga 2, colonna 27, indicato da (2, 27) nella direttiva. Il testo per DateTime.NowAndThen inizia nella colonna 15 di page.g.cs, come indicato da 15 nella direttiva. Il compilatore segnala l'errore nella sua 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. Il compilatore deve supportare i pragma usati. 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. Il #pragma warning disable format e il #pragma warning enable format controllano il modo in cui Visual Studio formatta i blocchi di codice.

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

Dove warning-list è un elenco delimitato da virgole di numeri di avviso, ad esempio 414, CS3021. 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()
    {
    }
}

Un'altra forma del pragma warning disabilita o ripristina i comandi di formattazione di Visual Studio in blocchi di codice:

#pragma warning disable format
#pragma warning restore format

I comandi di formato di Visual Studio non modificano il testo in blocchi di codice in cui disable format è attivo. I comandi di formato, ad esempio CTRL+K, CTRL+D, non modificano tali aree di codice. Questo pragma consente di controllare correttamente la presentazione visiva del codice.

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