C#-Präprozessoranweisungen

Obwohl der Compiler keinen separaten Präprozessor hat, werden die in diesem Abschnitt beschriebenen Anweisungen verarbeitet, als gäbe es einen. Sie werden zur Unterstützung der bedingten Kompilierung verwendet. Sie können diese Anweisungen im Gegensatz zu C- und C++-Anweisungen nicht verwenden, um Makros zu erstellen. Eine Präprozessordirektive muss die einzige Anweisung in einer Zeile sein.

Nullable-Kontext

Die Präprozessoranweisung #nullable legt den Nullable-Anmerkungskontext und den Nullable-Warnungskontext fest. Die Anweisung steuert, ob Nullable-Anmerkungen wirksam sind und ob Warnungen zur NULL-Zulässigkeit angegeben werden. Jeder Kontext ist entweder deaktiviert oder aktiviert.

Beide Kontexte können auf Projektebene (außerhalb des C#-Quellcodes) festgelegt werden. Die #nullable-Anweisung steuert die Anmerkungs- und Warnungskontexte und hat Vorrang vor anderen Einstellungen auf Projektebene. Eine Anweisung legt die von ihr gesteuerten Kontexte fest, bis sie von einer anderen Anweisung überschrieben wird oder bis zum Ende der Quelldatei.

Die Auswirkungen der Anweisungen lauten wie folgt:

  • #nullable disable: Diese Anweisung legt die Nullable-Anmerkungskontexte und -Warnungskontexte auf deaktiviert fest.
  • #nullable enable: Diese Anweisung legt die Nullable-Anmerkungskontexte und -Warnungskontexte auf aktiviert fest.
  • #nullable restore: Diese Anweisung stellt die Nullable-Anmerkungskontexte und -Warnungskontexte der Projekteinstellungen wieder her.
  • #nullable disable annotations: Diese Anweisung legt den Nullable-Anmerkungskontext auf deaktiviert fest.
  • #nullable enable annotations: Diese Anweisung legt den Nullable-Anmerkungskontext auf aktiviert fest.
  • #nullable restore annotations: Diese Anweisung stellt den Nullable-Anmerkungskontext der Projekteinstellungen wieder her.
  • #nullable disable warnings: Diese Anweisung legt den Nullable-Warnungskontext auf deaktiviert fest.
  • #nullable enable warnings: Diese Anweisung legt den Nullable-Warnungskontext auf aktiviert fest.
  • #nullable restore warnings: Diese Anweisung stellt den Nullable-Warnungskontext der Projekteinstellungen wieder her.

Bedingte Kompilierung

Die bedingte Kompilierung wird über die folgenden vier Präprozessoranweisungen gesteuert:

  • #if: Öffnet eine bedingte Kompilierung, bei der Code nur dann kompiliert wird, wenn das angegebene Symbol definiert ist.
  • #elif: Schließt die vorangehende bedingte Kompilierung und öffnet eine neue bedingte Kompilierung, wenn das angegebene Symbol definiert ist.
  • #else: Schließt die vorangehende bedingte Kompilierung und öffnet eine neue bedingte Kompilierung, wenn das angegebene Symbol nicht definiert ist.
  • #endif: Schließt die vorangehende bedingte Kompilierung.

Findet der C#-Compiler eine #if-Anweisung, die mit einer #endif-Anweisung beendet wird, wird der Code zwischen den Anweisungen nur dann kompiliert, wenn das angegebene Symbol definiert ist. Im Gegensatz zu C und C++ können Sie einem Symbol keinen numerischen Wert zuweisen. Die #if-Anweisung in C# ist ein boolescher Wert und überprüft nur, ob das Symbol definiert wurde. Beispiel:

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

Sie können die Operatoren == (Gleichheit) und != (Ungleichheit) zum Testen auf die bool-Werte true oder false verwenden. true bedeutet, dass das Symbol definiert wurde. Die #if DEBUG-Anweisung hat die gleiche Bedeutung wie #if (DEBUG == true). Sie können die Operatoren && (und), || (oder) und ! (nicht) verwenden, um auszuwerten, ob mehrere Symbole definiert wurden. Symbole und Operatoren können auch mit Klammern gruppiert werden.

Wenn Sie #if mit den Direktiven #else, #elif, #endif, #define und #undef verwenden, können Sie Code je nach dem Vorhandensein eines oder mehrerer Symbole ein- oder ausschließen. Die bedingte Kompilierung kann hilfreich sein, wenn Code für einen Debugbuild oder für eine bestimmte Konfiguration kompiliert wird.

Eine bedingte Anweisung, die mit einer #if-Anweisung beginnt, muss explizit mit einer #endif-Anweisung beendet werden. Über #define können Sie ein Symbol definieren. Wird dieses Symbol als Ausdruck an die #if-Anweisung übergeben, wird der Ausdruck als true ausgewertet. Sie können ein Symbol auch mit der Compileroption DefineConstants definieren. Die Definition eines Symbols kann mit #undef aufgehoben werden. Der Gültigkeitsbereich eines mit #define erstellten Symbols ist die Datei, in der es definiert wurde. Zwischen einem Symbol, das mit DefineConstants oder mit #define definiert wird, und einer Variablen mit dem gleichen Namen kommt es zu keinem Konflikt. Das bedeutet, dass ein Variablenname nicht an eine Präprozessoranweisung übergeben werden sollte und ein Symbol nur von einer Präprozessoranweisung ausgewertet werden kann.

Mit #elif können zusammengesetzte bedingte Direktiven erstellt werden. Der #elif-Ausdruck wird ausgewertet, wenn weder der Ausdruck der vorangehenden #if-Anweisung noch der Ausdruck einer vorangehenden (optionalen) #elif-Anweisung als true ausgewertet wird. Wird ein #elif-Ausdruck als true ausgewertet, wird der gesamte Code zwischen der #elif-Anweisung und der nächsten bedingten Anweisung vom Compiler ausgewertet. Beispiel:

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

Mit #else können Sie eine zusammengesetzte bedingte Anweisung erstellen, sodass der Compiler, wenn keiner der Ausdrücke in den vorangehenden #if-Anweisungen oder (optional) #elif-Anweisungen als true ausgewertet wird, den gesamten Code zwischen #else und der nächsten #endif-Anweisung auswertet. #endif(#endif) muss die nächste Präprozessoranweisung nach #else sein.

#endif gibt das Ende einer bedingten Anweisung an, die mit der #if-Anweisung beginnt.

Das Buildsystem kennt zudem vordefinierte Präprozessorsymbole, die verschiedene Zielframeworks in Projekten im SDK-Format darstellen. Diese sind hilfreich, wenn Sie Anwendungen erstellen, die für mehr als eine .NET-Version bestimmt sind.

Zielframeworks Symbole Zusätzliche Symbole, die in .NET SDK 5 oder höher verfügbar sind
.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 oder höher (und .NET Core) NET, NET6_0, NET5_0, NETCOREAPP, NETCOREAPP3_1, NETCOREAPP3_0, NETCOREAPP2_2, NETCOREAPP2_1, NETCOREAPP2_0, NETCOREAPP1_1, NETCOREAPP1_0 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

Hinweis

  • Versionslose Symbole werden unabhängig von der Version definiert, die Sie als Ziel verwenden.
  • Versionsspezifische Symbole werden nur für die Version definiert, die Sie als Ziel verwenden.
  • Die <framework>_OR_GREATER-Symbole werden für die Zielversion und alle früheren Versionen definiert. Wenn Sie beispielsweise .NET Framework 2.0 als Ziel festgelegt haben, werden die folgenden Symbole definiert: NET20, NET20_OR_GREATER, NET11_OR_GREATER und NET10_OR_GREATER.
  • Diese unterscheiden sich von den Zielframework-Monikern (TFMs), die von der MSBuild-Eigenschaft TargetFramework und NuGet verwendet werden.

Hinweis

Für herkömmliche Projekte, die kein SDK-Format aufweisen, müssen Sie die Symbole für die bedingte Kompilierung für die verschiedenen Zielframeworks in Visual Studio über die Eigenschaftenseite des Projekts manuell konfigurieren.

Andere vordefinierte Symbole beinhalten die Konstanten DEBUG und TRACE. Sie können die für das Projekt festgelegten Werte mit #define überschreiben. Das DEBUG-Symbol beispielsweise wird abhängig von den Buildkonfigurationseigenschaften (Modus „Debug“ oder „Release“) automatisch festgelegt.

Im folgenden Beispiel wird gezeigt, wie Sie ein MYTEST-Symbol für eine Datei definieren und dann die Werte der Symbole MYTEST und DEBUG testen. Die Ausgabe dieses Beispiels hängt davon ab, ob Sie das Projekt im Konfigurationsmodus Debug oder Release erstellen.

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

Im folgenden Beispiel wird gezeigt, wie für andere Zielframeworks zu testen, damit Sie neuere APIs möglichst verwenden können:

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

Definieren von Symbolen

Mit den folgenden beiden Präprozessoranweisungen können Sie Symbole für die bedingte Kompilierung definieren oder eine Definition aufheben:

  • #define: Definiert ein Symbol.
  • #undef: Hebt die Definition eines Symbols auf.

Mit #define wird ein Symbol definiert. Wenn Sie das Symbol als Ausdruck verwenden, der an die #if-Anweisung übergeben wird, wird der Ausdruck als true ausgewertet, wie in folgendem Beispiel dargestellt:

#define VERBOSE

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

Hinweis

Die #define-Direktive kann nicht zur Deklaration konstanter Werte wie in C und C++ verwendet werden. Definieren Sie Konstanten in C# als statische Member einer Klasse oder einer Struktur. Wenn Sie über mehrere solcher Konstanten verfügen, erwägen Sie, eine separate "Constants"-Klasse zu erstellen.

Symbole können verwendet werden, um Bedingungen für die Kompilierung anzugeben. Ein Symbol kann entweder mit #if oder mit #elif überprüft werden. Für die bedingte Kompilierung kann auch ConditionalAttribute verwendet werden. Ein Symbol kann zwar definiert werden, aber es kann ihm kein Wert zugewiesen werden. Die #define-Direktive muss in einer Datei vor allen Anweisungen, bei denen es sich nicht um Präprozessordirektiven handelt, verwendet werden. Sie können ein Symbol auch mit der Compileroption DefineConstants definieren. Die Definition eines Symbols kann mit #undef aufgehoben werden.

Definieren von Bereichen

Sie können Codebereiche definieren, die mithilfe der beiden folgenden Präprozessoranweisungen in einer Gliederung reduziert werden können:

  • #region: Beginnt einen Bereich.
  • #endregion: Beendet einen Bereich.

Mit #region können Sie einen Codeblock festlegen, der bei Verwendung der Gliederungsfunktion des Code-Editors erweitert oder reduziert werden kann. Es ist bei längeren Codedateien praktischer, einen oder mehrere Bereiche zu reduzieren oder auszublenden, sodass Sie sich auf den Teil der Datei konzentrieren können, an dem Sie gerade arbeiten. Das folgende Beispiel veranschaulicht, wie Sie einen Bereich definieren:

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

Ein #region-Block muss mit einer #endregion-Anweisung beendet werden. Ein #region-Block kann sich nicht mit einem #if-Block überschneiden. Allerdings kann ein #region-Block in einen #if-Block und ein #if-Block in einen #region-Block geschachtelt werden.

Fehler- und Warnungsinformationen

Mit den folgenden Anweisungen weisen Sie den Compiler an, benutzerdefinierte Compilerfehler und -warnungen zu generieren und Zeileninformationen zu steuern:

  • #error: Generiert einen Compilerfehler mit einer angegebenen Meldung.
  • #warning: Generiert eine Compilerwarnung mit einer angegebenen Meldung.
  • #line: Ändert die Zeilennummer, die mit Compilermeldungen gedruckt wird.

Mit #error können Sie von einem bestimmten Ort in Ihrem Code aus eine benutzerdefinierte Fehlermeldung CS1029 generieren. Beispiel:

#error Deprecated code in this method.

Hinweis

Der Compiler behandelt #error version auf besondere Weise und meldet den Compilerfehler CS8304 mit einer Nachricht, die die verwendeten Compiler- und Sprachversionen enthält.

Mit #warning können Sie von einem bestimmten Ort in Ihrem Code aus eine Compilerwarnung CS1030 der Stufe 1 generieren. Beispiel:

#warning Deprecated code in this method.

Mit #line können Sie die Zeilennummer des Compilers und (optional) die Dateinamensausgabe für Fehler und Warnungen bearbeiten.

Das folgende Beispiel zeigt, wie Sie zwei Warnungen melden können, die Zeilennummern zugeordnet sind. Die #line 200-Anweisung erzwingt die Nummer 200 der nächsten Zeile (obwohl der Standardwert #6 ist), und bis zur nächsten #line-Anweisung wird der Dateiname als „Special“ gemeldet. Die #line default-Standardanweisung legt die Zeilennummerierung auf deren Standardnummerierung fest, bei der die Zeilen gezählt werden, die von der vorherigen Anweisung neu nummeriert wurden.

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

Bei der Kompilierung wird die folgende Ausgabe erzeugt:

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

Die #line-Anweisung könnte in einem automatischen Zwischenschritt im Buildprozess verwendet werden. Wenn beispielsweise Zeilen aus der ursprünglichen Quellcodedatei entfernt würden, Sie jedoch trotzdem möchten, dass der Compiler eine Ausgabe basierend auf der ursprünglichen Zeilennummerierung in der Datei generiert, könnten Sie Zeilen entfernen und anschließend die ursprüngliche Zeilennummerierung mit #line simulieren.

Die #line hidden-Anweisung blendet die aufeinanderfolgenden Zeilen im Debugger aus, sodass alle Zeilen zwischen einer #line hidden-Anweisung und der nächsten #line-Anweisung (vorausgesetzt, es handelt sich nicht um eine weitere #line hidden-Anweisung) übersprungen werden, wenn der Entwickler den Code durchläuft. Diese Option kann auch dazu verwendet werden, ASP.NET die Möglichkeit zu geben, zwischen benutzerdefiniertem und computergeneriertem Code zu unterscheiden. Obwohl ASP.NET der primäre Anwender dieser Funktion ist, werden sich wahrscheinlich mehr Quellgeneratoren diese zunutze machen.

Eine #line hidden-Anweisung hat keine Auswirkung auf Dateinamen oder Zeilennummern bei der Fehlerberichterstattung. Das bedeutet, wenn der Compiler in einem ausgeblendeten Block einen Fehler findet, meldet er den aktuellen Dateinamen und die Zeilennummer des Fehlers.

Die #line filename-Anweisung gibt den Dateinamen an, von dem Sie möchten, dass er in der Compilerausgabe erscheint. Standardmäßig wird der tatsächliche Name der Quellcodedatei verwendet. Der Dateiname muss in doppelten Anführungszeichen ("") und hinter einer Zeilennummer stehen.

Ab C# 10 können Sie eine neue Form der #line-Direktive verwenden:

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

Die Komponenten dieser Variante werden im Folgenden erläutert:

  • (1, 1): Die Startzeile und Spalte für das erste Zeichen in der Zeile, die der Richtlinie folgt. In diesem Beispiel wird die nächste Zeile als Zeile 1, Spalte 1 gemeldet.
  • (5, 60): Dies ist die Endzeile und -spalte für den markierten Bereich.
  • 10: Dies ist der Spaltenversatz, der benötigt wird, damit die #line-Anweisung wirksam wird. In diesem Beispiel wird die 10. Spalte als Spalte 1 gemeldet. An diesem Ort beginnt die Deklaration int b = 0;. Dieses Feld ist optional. Wenn es nicht angegeben wird, wird die Anweisung in der ersten Spalte wirksam.
  • "partial-class.g.cs": Dies ist der Name der Ausgabedatei.

Im vorherigen Beispiel wird die folgende Warnung generiert:

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

Nach der Neuzuordnung befindet sich die Variable b in der ersten Zeile bei Zeichen 6.

Domänenspezifische Sprachen (DSLs) verwenden dieses Format in der Regel, um eine bessere Zuordnung von der Quelldatei zur generierten C#-Ausgabe zu ermöglichen. Weitere Beispiele für dieses Format finden Sie im Abschnitt Featurespezifikation zu den Beispielen.

Pragmas

#pragma gibt dem Compiler spezielle Anweisungen für die Kompilierung der Datei, in der es auftritt. Die Anweisungen müssen vom Compiler unterstützt werden. Das heißt, Sie können mit #pragma keine benutzerdefinierten Präprozessoranweisungen erstellen.

#pragma pragma-name pragma-arguments

Dabei stellt pragma-name den Namen eines erkannten Pragmas und pragma-arguments die pragmaspezifischen Argumente dar.

#pragma warning

#pragma warning kann bestimmte Warnungen aktivieren oder deaktivieren.

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

Dabei ist warning-list eine durch Trennzeichen getrennte Liste mit Warnungsnummern. Das Präfix „CS“ ist optional. Wenn keine Warnzahlen angegeben werden, deaktiviert disable alle Warnungen und restore aktiviert sie.

Hinweis

Um Warnzahlen in Visual Studio zu suchen, erstellen Sie Ihr Projekt und suchen Sie nach den Warnzahlen im Fenster Ausgabe.

disable wirkt sich ab der nächsten Zeile der Quelldatei aus. Die Warnung wird in der Zeile nach restore wiederhergestellt. Enthält die Datei kein restore, werden die Warnungen in der ersten Zeile aller späteren Dateien derselben Kompilierung im Standardzustand wiederhergestellt.

// 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-Prüfsumme

Erstellt für Quelldateien Prüfsummen, um beim Debuggen von ASP.NET-Seiten zu helfen.

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

Dabei ist "filename" der Name der Datei, die auf Änderungen oder Updates überwacht werden soll, "{guid}" die GUID (Global Unique Identifier) für den Hashalgorithmus und "checksum_bytes" die Zeichenfolge von Hexadezimalziffern, die die Bytes der Prüfsumme darstellen. Dabei muss es sich um eine gerade Anzahl hexadezimaler Ziffern handeln. Eine ungerade Anzahl von Ziffern führt zu einer Warnung zur Kompilierzeit, und die Anweisung wird ignoriert.

Der Visual Studio-Debugger verwendet eine Prüfsumme, um sicherzustellen, dass es immer die richtige Quelle findet. Der Compiler berechnet die Prüfsumme für eine Quelldatei, und speichert das Ergebnis in der Program Database-Datei (PDB). Der Debugger verwendet anschließend die PDB-Datei, um sie mit der Prüfsumme zu vergleichen, die für die Quelldatei berechnet wird.

Diese Lösung funktioniert nicht bei ASP.NET-Projekten, weil die berechnete Prüfsumme für die generierte Quelldatei und nicht für die ASPX-Datei gilt. #pragma checksum stellt für ASP.NET-Seiten Unterstützung von Prüfsummen bereit, um dieses Problem zu beheben.

Wenn Sie ein ASP.NET-Projekt in Visual C# erstellen, enthält die generierte Quelldatei eine Prüfsumme für die ASPX-Datei, von der die Quelle generiert wird. Der Compiler schreibt anschließend diese Informationen in die PDB-Datei.

Findet der Compiler keine #pragma checksum-Anweisung in der Datei, berechnet er die Prüfsumme und schreibt den Wert in die PDB-Datei.

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