C#-förprocessordirektiv

Även om kompilatorn inte har någon separat förprocessor bearbetas de direktiv som beskrivs i det här avsnittet som om det fanns en. Du använder dem för att hjälpa till med villkorlig kompilering. Till skillnad från C- och C++-direktiv kan du inte använda dessa direktiv för att skapa makron. Ett förprocessordirektiv måste vara den enda instruktionen på en rad.

Nullbar kontext

Föreprocessordirektivet #nullable anger den nullbara kommentarskontexten och den nullbara varningskontexten. Det här direktivet styr om ogiltiga anteckningar har effekt och om nullabilitetsvarningar ges. Varje kontext är antingen inaktiverad eller aktiverad.

Båda kontexterna kan anges på projektnivå (utanför C#-källkoden) och lägga till elementet Nullable i elementet PropertyGroup . Direktivet #nullable styr antecknings- och varningskontexterna och har företräde framför inställningarna på projektnivå. Ett direktiv anger de kontexter som det styr tills ett annat direktiv åsidosätter det, eller till slutet av källfilen.

Effekterna av direktiven är följande:

  • #nullable disable: Anger att de nullbara antecknings- och varningskontexterna ska inaktiveras.
  • #nullable enable: Anger de nullbara antecknings- och varningskontexterna till aktiverade.
  • #nullable restore: Återställer de nullbara antecknings- och varningskontexterna till projektinställningarna.
  • #nullable disable annotations: Anger den nullbara anteckningskontexten till inaktiverad.
  • #nullable enable annotations: Anger den nullbara anteckningskontexten till aktiverad.
  • #nullable restore annotations: Återställer den nullbara kommentarskontexten till projektinställningarna.
  • #nullable disable warnings: Anger den nullbara varningskontexten till inaktiverad.
  • #nullable enable warnings: Anger den nullbara varningskontexten till aktiverad.
  • #nullable restore warnings: Återställer den nullbara varningskontexten till projektinställningarna.

Villkorsstyrd kompilering

Du använder fyra förprocessordirektiv för att styra villkorlig kompilering:

  • #if: Öppnar en villkorlig kompilering, där kod endast kompileras om den angivna symbolen har definierats.
  • #elif: Stänger den föregående villkorliga kompileringen och öppnar en ny villkorlig kompilering baserat på om den angivna symbolen har definierats.
  • #else: Stänger den föregående villkorliga kompileringen och öppnar en ny villkorlig kompilering om den tidigare angivna symbolen inte har definierats.
  • #endif: Stänger den föregående villkorliga kompileringen.

C#-kompilatorn kompilerar koden mellan #if direktivet och #endif direktivet endast om den angivna symbolen definieras eller inte definieras när den ! inte används. Till skillnad från C och C++ kan ett numeriskt värde till en symbol inte tilldelas. -instruktionen #if i C# är boolesk och testar endast om symbolen har definierats eller inte. Följande kod kompileras till exempel när DEBUG har definierats:

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

Följande kod kompileras när MYTEST inte har definierats:

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

Du kan använda operatorerna == (likhet) och != (ojämlikhet) för att testa värdena booltrue eller false. true betyder att symbolen har definierats. -instruktionen #if DEBUG har samma betydelse som #if (DEBUG == true). Du kan använda operatorerna && (och), || (eller)och ! (inte) för att utvärdera om flera symboler har definierats. Du kan också gruppera symboler och operatorer med parenteser.

Följande är ett komplext direktiv som gör att koden kan dra nytta av nyare .NET-funktioner samtidigt som den är bakåtkompatibel. Anta till exempel att du använder ett NuGet-paket i koden, men paketet stöder bara .NET 6 och uppåt, samt .NET Standard 2.0 och uppåt:

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

#ifMed , tillsammans med direktiven #else, #elif, #endif, #defineoch #undef kan du inkludera eller exkludera kod baserat på förekomsten av en eller flera symboler. Villkorsstyrd kompilering kan vara användbar när du kompilerar kod för en felsökningsversion eller vid kompilering för en specifik konfiguration.

Ett villkorsdirektiv som börjar med ett #if direktiv måste uttryckligen avslutas med ett #endif direktiv. #define låter dig definiera en symbol. Genom att använda symbolen som uttrycket som skickas till #if direktivet utvärderas uttrycket till true. Du kan också definiera en symbol med kompilatoralternativet DefineConstants . Du kan odefiniera en symbol med #undef. Omfånget för en symbol som skapats med #define är den fil där den definierades. En symbol som du definierar med DefineConstants eller med står inte i konflikt med #define en variabel med samma namn. Det betyder att ett variabelnamn inte ska skickas till ett förprocessordirektiv, och en symbol kan bara utvärderas av ett förprocessordirektiv.

#elif låter dig skapa ett sammansatt villkorligt direktiv. Uttrycket #elif utvärderas om varken föregående #if eller några föregående, valfria #elif direktivuttryck utvärderas till true. Om ett #elif uttryck utvärderas till trueutvärderar kompilatorn all kod mellan #elif och nästa villkorsdirektiv. Till exempel:

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

#else låter dig skapa ett sammansatt villkorligt direktiv, så att om inget av uttrycken i föregående #if eller (valfria) #elif direktiv utvärderas till trueutvärderar kompilatorn all kod mellan #else och nästa #endif. #endif(#endif) måste vara nästa förprocessordirektiv efter #else.

#endif anger slutet på ett villkorsdirektiv som började med #if direktivet.

Byggsystemet är också medvetet om fördefinierade förprocessorsymboler som representerar olika målramverk i SDK-liknande projekt. De är användbara när du skapar program som kan rikta in sig på mer än en .NET-version.

Målramverk Symboler Ytterligare symboler
(finns i .NET 5+ SDK:er)
Plattformssymboler (endast tillgängliga
när du anger en OS-specifik TFM)
.NET Framework NETFRAMEWORK, NET48, NET472, NET471, NET47, NET462, NET461, NET46, , NET452, NET451, NET45, NET40, , NET35NET20 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_GREATERNET20_OR_GREATER
.NET Standard NETSTANDARD, NETSTANDARD2_1, NETSTANDARD2_0, NETSTANDARD1_6, NETSTANDARD1_5, NETSTANDARD1_4, , NETSTANDARD1_3, NETSTANDARD1_2, , NETSTANDARD1_1NETSTANDARD1_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_GREATERNETSTANDARD1_0_OR_GREATER
.NET 5+ (och .NET Core) NET, NET8_0, NET7_0, NET6_0, NET5_0, NETCOREAPP, NETCOREAPP3_1, NETCOREAPP3_0, , NETCOREAPP2_2, NETCOREAPP2_1, NETCOREAPP2_0, , , NETCOREAPP1_1NETCOREAPP1_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_GREATERNETCOREAPP1_0_OR_GREATER ANDROID, BROWSER, IOS, MACCATALYST, MACOS, , TVOS, , WINDOWS
[OS][version] (till exempel IOS15_1),
[OS][version]_OR_GREATER (till exempel IOS15_1_OR_GREATER)

Kommentar

  • Versionslösa symboler definieras oavsett vilken version du riktar in dig på.
  • Versionsspecifika symboler definieras endast för den version som du riktar in dig på.
  • Symbolerna <framework>_OR_GREATER definieras för den version som du riktar in dig på och alla tidigare versioner. Om du till exempel riktar in dig på .NET Framework 2.0 definieras följande symboler: NET20, NET20_OR_GREATER, NET11_OR_GREATERoch NET10_OR_GREATER.
  • Symbolerna NETSTANDARD<x>_<y>_OR_GREATER definieras endast för .NET Standard-mål och inte för mål som implementerar .NET Standard, till exempel .NET Core och .NET Framework.
  • Dessa skiljer sig från målramverksmonikers (TFM: er) som används av egenskapen MSBuild TargetFramework och NuGet.

Kommentar

För traditionella, icke-SDK-liknande projekt måste du manuellt konfigurera de villkorliga kompileringssymbolerna för de olika målramverken i Visual Studio via projektets egenskapssidor.

Andra fördefinierade symboler är konstanterna DEBUG och TRACE . Du kan åsidosätta de värden som angetts för projektet med hjälp av #define. DEBUG-symbolen anges till exempel automatiskt beroende på byggkonfigurationsegenskaperna ("Felsökning" eller "Release"-läge).

I följande exempel visas hur du definierar en MYTEST symbol i en fil och sedan testar värdena för symbolerna MYTEST och DEBUG . Utdata från det här exemplet beror på om du har skapat projektet i felsöknings- eller versionskonfigurationsläge.

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

I följande exempel visas hur du testar för olika målramverk så att du kan använda nyare API:er när det är möjligt:

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

Definiera symboler

Du använder följande två förprocessordirektiv för att definiera eller odefiniera symboler för villkorlig kompilering:

  • #define: Definiera en symbol.
  • #undef: Odefiniera en symbol.

Du använder #define för att definiera en symbol. När du använder symbolen som det uttryck som skickas till #if direktivet utvärderas uttrycket till true, som följande exempel visar:

#define VERBOSE

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

Kommentar

Direktivet #define kan inte användas för att deklarera konstanta värden som normalt görs i C och C++. Konstanter i C# definieras bäst som statiska medlemmar i en klass eller struct. Om du har flera sådana konstanter kan du överväga att skapa en separat "Konstanter"-klass för att lagra dem.

Symboler kan användas för att ange villkor för kompilering. Du kan testa symbolen med antingen #if eller #elif. Du kan också använda ConditionalAttribute för att utföra villkorlig kompilering. Du kan definiera en symbol, men du kan inte tilldela ett värde till en symbol. Direktivet #define måste visas i filen innan du använder några instruktioner som inte också är förprocessordirektiv. Du kan också definiera en symbol med kompilatoralternativet DefineConstants . Du kan odefiniera en symbol med #undef.

Definiera regioner

Du kan definiera kodregioner som kan komprimeras i en disposition med hjälp av följande två förprocessordirektiv:

  • #region: Starta en region.
  • #endregion: Avsluta en region.

#region låter dig ange ett kodblock som du kan expandera eller komprimera när du använder kodredigerarens dispositionsfunktion. I längre kodfiler är det praktiskt att komprimera eller dölja en eller flera regioner så att du kan fokusera på den del av filen som du arbetar med. I följande exempel visas hur du definierar en region:

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

Ett #region block måste avslutas med ett #endregion direktiv. Ett #region block kan inte överlappa med ett #if block. Ett block kan dock #region kapslas i ett #if block och ett #if block kan kapslas i ett #region block.

Fel- och varningsinformation

Du instruerar kompilatorn att generera användardefinierade kompilatorfel och varningar samt kontrollradsinformation med hjälp av följande direktiv:

  • #error: Generera ett kompilatorfel med ett angivet meddelande.
  • #warning: Generera en kompilatorvarning med ett specifikt meddelande.
  • #line: Ändra radnumret som skrivs ut med kompilatormeddelanden.

#error låter dig generera ett användardefinierat CS1029-fel från en specifik plats i koden. Till exempel:

#error Deprecated code in this method.

Kommentar

Kompilatorn behandlar #error version på ett speciellt sätt och rapporterar ett kompilatorfel, CS8304, med ett meddelande som innehåller den använda kompilatorn och språkversionerna.

#warning låter dig generera en cs1030 nivå en kompilatorvarning från en specifik plats i koden. Till exempel:

#warning Deprecated code in this method.

#line låter dig ändra kompilatorns radnumrering och (valfritt) filnamnets utdata för fel och varningar.

I följande exempel visas hur du rapporterar två varningar som är associerade med radnummer. Direktivet #line 200 tvingar nästa rads tal att vara 200 (även om standardvärdet är #6), och fram till nästa #line direktiv rapporteras filnamnet som "Special". Direktivet #line default returnerar radnumreringen till standardnumreringen, som räknar de rader som numrerades om av föregående direktiv.

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

Kompilering ger följande utdata:

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

Direktivet #line kan användas i ett automatiserat, mellanliggande steg i byggprocessen. Om rader till exempel har tagits bort från den ursprungliga källkodsfilen, men du fortfarande vill att kompilatorn ska generera utdata baserat på den ursprungliga radnumreringen i filen, kan du ta bort rader och sedan simulera den ursprungliga radnumreringen med #line.

Direktivet #line hidden döljer efterföljande rader från felsökningsprogrammet, så att när utvecklaren går igenom koden kommer alla rader mellan ett #line hidden och nästa #line direktiv (förutsatt att det inte är ett annat #line hidden direktiv) att kliva över. Det här alternativet kan också användas för att tillåta ASP.NET att skilja mellan användardefinierad och maskingenererad kod. Även om ASP.NET är den primära konsumenten av den här funktionen är det troligt att fler källgeneratorer använder den.

Ett #line hidden direktiv påverkar inte filnamn eller radnummer i felrapportering. Om kompilatorn hittar ett fel i ett dolt block rapporterar kompilatorn det aktuella filnamnet och radnumret för felet.

Direktivet #line filename anger det filnamn som du vill ska visas i kompilatorns utdata. Som standard används det faktiska namnet på källkodsfilen. Filnamnet måste vara inom dubbla citattecken ("") och måste föregås av ett radnummer.

Från och med C# 10 kan du använda en ny form av #line direktivet:

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

Komponenterna i det här formuläret är:

  • (1, 1): Startraden och kolumnen för det första tecknet på raden som följer direktivet. I det här exemplet rapporteras nästa rad som rad 1, kolumn 1.
  • (5, 60): Slutraden och kolumnen för den markerade regionen.
  • 10: Kolumnförskjutningen för #line att direktivet ska börja gälla. I det här exemplet rapporteras den tionde kolumnen som kolumn ett. Det är där deklarationen int b = 0; börjar. Det här fältet är valfritt. Om det utelämnas börjar direktivet gälla för den första kolumnen.
  • "partial-class.cs": Namnet på utdatafilen.

Föregående exempel skulle generera följande varning:

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

Efter ommappning är variabeln , bpå den första raden, på tecken sex, i filen partial-class.cs.

Domänspecifika språk (DSL:er) använder vanligtvis det här formatet för att ge en bättre mappning från källfilen till de genererade C#-utdata. Den vanligaste användningen av det här utökade #line direktivet är att mappa om varningar eller fel som visas i en genererad fil till den ursprungliga källan. Tänk dig till exempel den här rakbladssidan:

@page "/"
Time: @DateTime.NowAndThen

Egenskapen DateTime.Now angavs felaktigt som DateTime.NowAndThen. Det genererade C# för det här rakbladsfragmentet ser ut så här i page.g.cs:

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

Kompilatorns utdata för föregående kodfragment är:

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

Rad 2, kolumn 6 i page.razor är den plats där texten @DateTime.NowAndThen börjar. Det framgår av (2, 6) direktivet. Det intervallet slutar @DateTime.NowAndThen på rad 2, kolumn 27. Det framgår av (2, 27) direktivet. Texten för DateTime.NowAndThen börjar i kolumn 15 i page.g.cs. Det framgår av 15 direktivet. När du sätter ihop alla argument rapporterar kompilatorn felet på dess plats i page.razor. Utvecklaren kan navigera direkt till felet i källkoden, inte till den genererade källan.

Mer information om det här formatet finns i funktionsspecifikationen i avsnittet om exempel.

Pragmas

#pragma ger kompilatorn särskilda instruktioner för kompilering av filen där den visas. Instruktionerna måste stödjas av kompilatorn. Med andra ord kan du inte använda #pragma för att skapa anpassade förbearbetningsinstruktioner.

#pragma pragma-name pragma-arguments

Var pragma-name är namnet på en erkänd pragma och pragma-arguments är de pragma-specifika argumenten.

#pragma varning

#pragma warning kan aktivera eller inaktivera vissa varningar.

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

Var warning-list finns en kommaavgränsad lista med varningsnummer. "CS"-prefixet är valfritt. När inga varningsnummer har angetts disable inaktiverar du alla varningar och restore aktiverar alla varningar.

Kommentar

Om du vill hitta varningsnummer i Visual Studio skapar du projektet och letar sedan efter varningsnumren i utdatafönstret.

Börjar disable gälla från och med nästa rad i källfilen. Varningen återställs på raden efter restore. Om det inte finns någon restore i filen återställs varningarna till standardtillståndet på den första raden i senare filer i samma kompilering.

// 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()
    {
    }
}

#pragma kontrollsumma

Genererar kontrollsummor för källfiler för felsökning av ASP.NET sidor.

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

Där "filename" är namnet på filen som kräver övervakning för ändringar eller uppdateringar, "{guid}" är den globalt unika identifieraren (GUID) för hash-algoritmen och "checksum_bytes" är strängen med hexadecimala siffror som representerar kontrollsummans byte. Måste vara ett jämnt antal hexadecimala siffror. Ett udda antal siffror resulterar i en kompileringstidsvarning och direktivet ignoreras.

Visual Studio-felsökaren använder en kontrollsumma för att se till att den alltid hittar rätt källa. Kompilatorn beräknar kontrollsumman för en källfil och genererar sedan utdata till programdatabasfilen (PDB). Felsökningsprogrammet använder sedan PDB för att jämföra med den kontrollsumma som den beräknar för källfilen.

Den här lösningen fungerar inte för ASP.NET projekt, eftersom den beräknade kontrollsumman är för den genererade källfilen i stället för den .aspx filen. För att lösa det här problemet #pragma checksum tillhandahåller checksummor stöd för ASP.NET sidor.

När du skapar ett ASP.NET projekt i Visual C# innehåller den genererade källfilen en kontrollsumma för den .aspx fil som källan genereras från. Kompilatorn skriver sedan den här informationen till PDB-filen.

Om kompilatorn inte hittar något #pragma checksum direktiv i filen beräknas kontrollsumman och värdet skrivs till PDB-filen.

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