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 , 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
eNET10_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 dichiarazioneint 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 warning
: abilitare o disabilitare gli avvisi.#pragma checksum
: genera un checksum.
#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
}
}