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
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").
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 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 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 dichiarazioneint 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 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. 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
}
}