共用方式為


C# 前置處理器指示詞

雖然編譯器沒有另外的前置處理器,但處理本節中所述的指示詞時,會像是有前置處理器一樣。 您可以使用這些指示詞來協助條件式編譯。 不同於 C 和 C++ 指示詞,您無法使用這些指示詞來建立巨集。 每個前置處理器指示詞都必須是該行中的唯一指令。

檔案型應用程式

檔案型應用程式是使用 (或任何dotnet run Program.cs檔案) 編*.cs譯和執行的程式。 C# 編譯程式會忽略這些預處理器指示詞,但建置系統會剖析它們以產生輸出。 這些指令在進行專案型編譯時遇到時會產生警告。

C# 編譯程式會忽略開頭為 #:#!的任何預處理器指示詞。

#!預處理器指示詞可讓 unix 殼層使用 dotnet run直接執行 C# 檔案。 例如:

#!/usr/bin/env dotnet run
Console.WriteLine("Hello");

上述代碼段會通知 Unix 殼層使用 dotnet run來執行檔案。 這個 /usr/bin/env 指令會在 dotnet 你的 PATH 中定位執行檔,使此方法能在不同 Unix 和 macOS 發行版間移植。 這 #! 一行必須是檔案中的第一行,而下列標記是要執行的程式。 您必須針對該功能啟用 C# 檔案的執行 (x) 權限。

#:檔案型應用程式中使用的指示詞包括:

  • #:sdk

    第一個實例會指定節點的值 <Project Sdk="value" /> 。 後續實例會指定 <Sdk Name="value" Version="version" /> 節點。 可以省略版本 (,亦即,如果在 global.json 中指定或包含在 .NET SDK 中)。 例如:

    #:sdk Microsoft.NET.Sdk.Web
    #:sdk Aspire.AppHost.Sdk@9.4.1
    

    上述兩個前置處理器會轉譯為:

    <Project Sdk="Microsoft.NET.Sdk.Web" />
        <Sdk Name="Aspire.AppHost.Sdk" Version="9.4.1" />
    
  • #:property

    #:property 的實例會被轉譯成 <PropertyGroup> 中的屬性元素。 表單 Name=value 的令牌必須遵循 property 令牌。 下列範例指令是有效的 property 令牌:

    #:property TargetFramework=net11.0
    #:property LangVersion=preview
    

    上述兩個屬性會轉譯成:

    <TargetFramework>net11.0</TargetFramework>
    <LangVersion>preview</LangVersion>
    
  • #:package

    #:package 實例會轉譯成 PackageReference 元素,以將具有指定版本的 NuGet 套件包含在檔案中。 例如:

    #:package System.CommandLine@2.0.0-*
    

    上述預處理器令牌會轉譯成:

    <PackageReference Include="System.CommandLine" Version="2.0.0-*">
    
  • #:project

    #:project 實例會轉譯成 ProjectReference 元素,以包含具有指定專案路徑的專案。 例如:

    #:project ../Path/To.Example
    

    上述預處理器令牌會轉譯成:

    <ProjectReference Include="../Path/To.Example/To.Example.csproj" />
    

工具可以遵循 #: 慣例來新增令牌。

可為 Null 的內容

#nullable 預處理器指示詞會設定 批註警告 旗標 可為 Null 的內容。 這個指示詞會控制可為 Null 的注釋是否有效,以及是否提供可為 Null 的警告。 每個旗標都會 停用啟用

這兩個內容都可以專案層級指定 (在 C# 原始程式碼外部),並將 Nullable 元素新增至 PropertyGroup 元素。 #nullable 指令用來控制註釋和警告旗標,並且優先於專案層級設定。 指令會設定其控制的旗標,直到另一個指令覆寫它,或直到來源檔案的結尾。

指示詞的效果如下所示:

  • #nullable disable:將可為 Null 的內容 設定為停用
  • #nullable enable:將可為 Null 的內容 設定為啟用
  • #nullable restore:將可為 Null 的上下文還原至項目設定。
  • #nullable disable annotations:將可為 Null 內容中的批註旗標設定為 停用
  • #nullable enable annotations:在可空性上下文中,將批註旗標設定為 啟用
  • #nullable restore annotations:在可空值上下文中將註解標記復原至專案設定。
  • #nullable disable warnings:將可為 Null 值內容中的警告旗標設定為 關閉
  • #nullable enable warnings:將可為 Null 的內容中的警告旗標設定為 已啟用
  • #nullable restore warnings:將可為 Null 的內容中的警告標誌還原至專案設定。

條件式編譯

您可以使用四個預處理器指示詞來控制條件式編譯:

  • #if:開啟條件式編譯,其中只有在定義指定的符號時,才會編譯器代碼。
  • #elif:關閉上述條件式編譯,並根據定義指定的符號來開啟新的條件式編譯。
  • #else:關閉上述條件式編譯,如果先前指定的符號尚未定義,則開啟新的條件式編譯。
  • #endif:關閉先前的條件式編譯。

建置系統也能分辨 SDK 樣式專案中代表不同目標 Framework 的預先定義前置處理器符號。 若要建立能以多個 .NET 版本為目標的應用程式,這些符號就很實用。

目標 Framework 符號 其他符號
(適用於 .NET 5+ SDK)
平台符號 (僅可於
指定 OS 特定 TFM 後使用)
.NET Framework NETFRAMEWORKNET481NET48NET472NET471NET47NET462NET461NET46NET452NET451NET45NET40NET35NET20 NET48_OR_GREATERNET472_OR_GREATERNET471_OR_GREATERNET47_OR_GREATERNET462_OR_GREATERNET461_OR_GREATERNET46_OR_GREATERNET452_OR_GREATERNET451_OR_GREATERNET45_OR_GREATERNET40_OR_GREATERNET35_OR_GREATERNET20_OR_GREATER
.NET Standard NETSTANDARDNETSTANDARD2_1NETSTANDARD2_0NETSTANDARD1_6NETSTANDARD1_5NETSTANDARD1_4NETSTANDARD1_3NETSTANDARD1_2NETSTANDARD1_1NETSTANDARD1_0 NETSTANDARD2_1_OR_GREATERNETSTANDARD2_0_OR_GREATERNETSTANDARD1_6_OR_GREATERNETSTANDARD1_5_OR_GREATERNETSTANDARD1_4_OR_GREATERNETSTANDARD1_3_OR_GREATERNETSTANDARD1_2_OR_GREATERNETSTANDARD1_1_OR_GREATERNETSTANDARD1_0_OR_GREATER
.NET 5+ (和 .NET Core) NETNET10_0NET9_0NET8_0NET7_0NET6_0NET5_0NETCOREAPPNETCOREAPP3_1NETCOREAPP3_0NETCOREAPP2_2NETCOREAPP2_1NETCOREAPP2_0NETCOREAPP1_1NETCOREAPP1_0 NET10_0_OR_GREATERNET9_0_OR_GREATERNET8_0_OR_GREATERNET7_0_OR_GREATERNET6_0_OR_GREATERNET5_0_OR_GREATERNETCOREAPP3_1_OR_GREATERNETCOREAPP3_0_OR_GREATERNETCOREAPP2_2_OR_GREATERNETCOREAPP2_1_OR_GREATERNETCOREAPP2_0_OR_GREATERNETCOREAPP1_1_OR_GREATERNETCOREAPP1_0_OR_GREATER ANDROIDBROWSERIOSMACCATALYSTMACOSTVOS、、、 WINDOWS
[OS][version] (例如 IOS15_1)、
[OS][version]_OR_GREATER (例如 IOS15_1_OR_GREATER)

注意

  • 無論您的目標版本為何,系統都會定義無版本符號。
  • 系統只會針對您設定為目標的版本定義版本特定符號。
  • 系統會針對您的目標版本和所有更早版本定義 <framework>_OR_GREATER 符號。 舉例來說,如果您要以 .NET Framework 2.0 為目標,則會定義下列符號:NET20NET20_OR_GREATERNET11_OR_GREATERNET10_OR_GREATER
  • NETSTANDARD<x>_<y>_OR_GREATER 符號只會針對 .NET Standard 目標定義,而不是針對實作 .NET Standard 的目標,例如 .NET Core 和 .NET Framework。
  • 這些與 MSBuild TargetFramework 屬性NuGet 所使用的目標 Framework Moniker (TFM) 不同。

注意

針對傳統的非 SDK 樣式專案,您必須透過專案的屬性頁面,手動為 Visual Studio 中不同目標架構設定條件式編譯符號。

其他預先定義符號包括 DEBUGTRACE 常數。 您可以使用 #define 來覆寫為專案所設定的值。 例如,DEBUG 符號會根據您的組建組態屬性 ("Debug" 或 "Release" 模式) 而自動設定。

C# 編譯器只會在定義指定的符號時,或在未使用 #if not 運算子時,編譯 #endif 指示詞和 ! 指示詞之間的程式碼。 不同於 C 和 C++,無法指派符號的數值。 C# 中的 #if 語句是布爾值,而且只會測試是否已定義符號。 例如,定義時 DEBUG 會編譯下列程式碼:

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

MYTEST定義 時,會編譯下列程式代碼:

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

您能使用運算子 == (相等) != (不等) 來測試 booltruefalsetrue 表示已定義符號。 #if DEBUG 陳述式的意義與 #if (DEBUG == true) 一樣。 您可以使用 && (and)|| (或)! (not) 運算符來評估是否已定義多個符號。 您也可以使用括弧來將符號和運算子分組。

下列範例顯示一個複雜的指令,允許您的程式代碼利用較新的 .NET 功能,同時保持向後相容。 例如,假設您在程式碼中使用 NuGet 套件,但套件僅支援 .NET 6 和更新版本,以及 .NET Standard 2.0 和更新版本:

#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 連同 #else#elif#endif#define#undef 指示詞,可讓您根據一或多個符號是否存在來包含或排除程式碼。 在編譯偵錯組建的程式碼時,或是在針對特定組態進行編譯時,條件式編譯非常實用。

#elif 可讓您建立複合條件指示詞。 如果上述 #elif 和任何先前、選擇性 #if 指示詞運算式都不評估為 #elif,則會評估 true 表達式。 如果 #elif 運算式評估為 true,編譯器會評估 #elif 與下一個條件指示詞之間的所有程式碼。 例如:

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

#else 可讓您建立複合條件指示詞,如此,如果前面的 #if 或 (選擇性) #elif 指示詞中沒有運算式評估為 true,則編譯器會評估 #else 與後續 #endif 之間的所有程式碼。 #endif(#endif) 必須是 #else 後面的下一個前置處理器指示詞。

#endif 指定條件指示詞的結尾,而其開始於 #if 指示詞。

以下範例說明如何在檔案上定義 MYTEST 符號,然後測試 MYTESTDEBUG 符號的值。 此範例的輸出視您使用 DebugRelease 組態模式建置專案而有所不同。

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

以下範例說明如何測試不同的目標 Framework,讓您可以盡可能使用較新的 API:

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

定義符號

您可以使用下列兩個預處理器指示詞來定義或取消定義條件式編譯的符號:

  • #define:定義符號。
  • #undef:取消符號的定義。

您可以使用 #define 來定義符號。 當您使用符號作為傳遞至 #if 指令的表達式時,表達式會評估為 true,如下列範例所示:

#define VERBOSE

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

注意

在 C# 中,應該使用 const 關鍵字來定義基本常數。 const 宣告會建立無法在執行階段修改的 static 成員。 如果常數值通常是在 C 和 C++ 中進行宣告,您就不能使用 #define 指示詞進行宣告。 如果您有數個這類常數,請考慮建立個別的「常數」類別來保留它們。

符號可以用來指定編譯的條件。 您可以使用 #if#elif 來測試符號。 您也可以使用 ConditionalAttribute 執行條件式編譯。 您可以定義符號,但不能將值指派給符號。 如果您要使用的任何指示並不是前置處理器指示詞,則檔案中必須先出現 #define 指示詞才行。 您也可以使用 DefineConstants 編譯器選項來定義符號。 您可以使用 #undef 來取消定義符號。

定義區域

您可以使用下列兩個預處理器指示詞,定義大綱中可折疊的程式碼區域:

  • #region:啟動區域。
  • #endregion:結束區域。

#region 可讓您指定程式碼區塊,當您使用程式碼編輯器的大綱時,可以展開或摺疊該程式碼區塊。 在較長的程式碼檔案中,摺疊或隱藏一或多個區域是很方便的,如此您可以專注於目前處理的檔案部分。 下例示範如何定義區域:

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

#region 區塊必須以 #endregion 指示詞結束。 #region 區塊無法與 #if 區塊重疊。 不過,#region 區塊可以巢狀方式置於一個 #if 區塊中,而一個 #if 區塊可以巢狀方式置於 #region 區塊中。

錯誤和警告資訊

您可以指示編譯器使用下列指示詞產生使用者定義的編譯器錯誤和警告,以及控制行資訊:

  • #error:產生具有指定訊息的編譯器錯誤。
  • #warning:產生具有特定訊息的編譯器警告。
  • #line:變更以編譯器訊息列印的行號。

#error 可讓您從您程式碼中的特定位置產生 CS1029 使用者定義錯誤。 例如:

#error Deprecated code in this method.

注意

編譯器會以特殊方式處理 #error version,並報告編譯器錯誤 CS8304,其中包含使用的編譯器和語言版本的訊息。

#warning 可讓您從程式碼中的特定位置產生 CS1030 層級一的編譯器警告。 例如:

#warning Deprecated code in this method.

#line 可讓您修改編譯器的行號以及 (選擇性) 錯誤和警告的檔案名稱輸出。

下列範例示範如何報告兩個與行號建立關聯的警告。 #line 200 指示詞會強制下一行的行號變為 200(雖然預設值是 #6),在下一個 #line 指示詞之前,檔案名稱會被報告為「Special」。#line default 指示詞會將行號恢復為預設編號,這會計算由前一個指示詞重新編號的行數。

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

編譯會產生下列輸出:

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

#line 指示詞可以用於建置程序中的自動化中繼步驟。 例如,如果已從原始程式碼檔中移除行,但您仍然想要編譯器根據檔案中的原始行編號來產生輸出,則可以移除行,然後模擬具有 #line 的原始行編號。

#line hidden 指示詞會隱藏偵錯工具中的後續行,如此一來,開發人員逐步執行程式碼時,會逐步執行 #line hidden 與下一個 #line 指示詞 (假設它不是另一個 #line hidden 指示詞) 之間的任何行。 此選項也可用來讓 ASP.NET 區分使用者定義的程式碼與電腦產生的程式碼。 雖然 ASP.NET 是此功能的主要取用者,但可能有更多的來源產生器使用它。

#line hidden 指示詞不會影響錯誤報告中的檔案名稱或行號。 也就是說,如果編譯程式在隱藏區塊中發現錯誤,編譯程式會報告目前的檔名和錯誤行號。

#line filename 指示詞指定您想要在編譯器輸出中顯示的檔案名稱。 預設會使用原始程式碼檔的實際名稱。 檔名必須以雙引號 (“”) 括住,且必須遵循行號。

您可以使用 #line 指令的新形式:

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

此表單的元件包括:

  • (1, 1):指令後一行的首個字元所在的起始行和起始列。 在此範例中,下一行會回報為第 1 行,第 1 欄。
  • (5, 60):標示區域的結束行和資料行。
  • 10#line 指示詞要生效的資料行位移。 在此範例中,第 10 個資料行會回報為第一欄。 宣告 int b = 0; 從該欄開始。 這是選用欄位。 如果省略,指示詞會對第一個資料行生效。
  • "partial-class.cs":輸出檔的名稱。

上述範例會產生下列警告:

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

在重新對應之後,變數 b 位於檔案 partial-class.cs 的第一行,第六個字元。

網域特定語言 (DSL) 通常會使用此格式,以提供更好的來源檔案對應至產生的 C# 輸出。 此擴充 #line 指示詞最常見的用法是將產生的檔案中出現的警告或錯誤重新對應至原始來源。 例如,請參考這個 Razor 頁面:

@page "/"
Time: @DateTime.NowAndThen

屬性 DateTime.Now 輸入錯誤為 DateTime.NowAndThen。 在 page.g.cs 中,這個 Razor 程式碼片段產生的 C# 看起來如下:

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

上述程式碼片段的編譯器輸出為:

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

第 2 行,第 6 列在 page.razor 中是文字 @DateTime.NowAndThen 開始的位置,由指令中的 (2, 6) 指出。 該範圍 @DateTime.NowAndThen 結束於第 2 行第 27 列,並在指令中由 (2, 27) 標示。 DateTime.NowAndThen 的文字從 page.g.cs的第 15 欄開始,指示中的 15 有標示出。 編譯程式會在 page.razor中報告其位置的錯誤。 開發人員可以直接導覽至其原始程式碼中的錯誤,而不是產生的來源。

若要查看此格式的更多範例,請參閱範例一節中的功能規格

Pragma

#pragma 將編譯編譯器所在檔案的特殊指示提供給編譯器。 編譯程式必須支持您使用的 pragma 指令。 換句話說,您不能使用 #pragma 來建立自訂前置處理指示。

#pragma pragma-name pragma-arguments

其中 pragma-name 是已辨識 pragma 的名稱,而 pragma-arguments 是 pragma 特定的引數。

#pragma 警告

#pragma warning 可以啟用或停用特定警告。 #pragma warning disable format#pragma warning enable format 控制 Visual Studio 程式代碼區塊的格式。

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

其中 warning-list 是警告編號的逗號分隔清單,例如 414, CS3021。 "CS" 前置詞是選擇性的。 未指定警告編號時,disable 會停用所有警告,而 restore 會啟用所有警告。

注意

若要尋找 Visual Studio 中的警告編號,請建立專案,然後在 [輸出] 視窗中尋找警告編號。

disable 會從來源檔案的下一行開始生效。 警告會在 restore 後面的這一行還原。 如果檔案中沒有 restore,警告會還原至相同編譯中任何稍後檔案的第一行的預設狀態。

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

warning pragma 的另一種形式會停用或還原程式代碼區塊中的 Visual Studio 格式化命令:

#pragma warning disable format
#pragma warning restore format

Visual Studio 格式命令不會在 disable format 生效的程式代碼區塊中修改文字。 格式命令,例如 Ctrl+KCtrl+D,不會修改這些程式代碼區域。 此 pragma 可讓您精細控制程式碼的視覺呈現。

#pragma 總和檢查碼

產生原始程式檔的總和檢查碼,協助偵錯 ASP.NET 頁面。

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

其中 "filename" 是需要監視變更或更新的檔案名,"{guid}" 是雜湊演算法的全域唯一識別碼 (GUID),而且 "checksum_bytes" 是代表總和檢查碼位元組的十六進位數位字串。 必須是偶數的十六進位數字。 奇數的數字會導致編譯時期警告,並且忽略指示詞。

Visual Studio 偵錯工具會使用總和檢查碼來確定一定會找到正確的來源。 編譯器會計算來源檔案的總和檢查碼,然後將輸出發至程式資料庫 (PDB) 檔案。 然後偵錯工具會使用 PDB 比較總和檢查碼計算來源檔案。

這個解決方案不適用於 ASP.NET 專案,因為計算過的總和檢查碼適合產生的來源檔案,不是 .aspx 檔案。 若要解決這個問題,#pragma checksum 會為 ASP.NET 頁面提供總和檢查碼支援。

當您在 Visual C# 中建立 ASP.NET 專案時,所產生原始程式檔會包含來源 .aspx 檔案的總和檢查碼。 接著編譯器會將這項資訊寫入 PDB 檔案中。

如果編譯器在檔案中找不到任何 #pragma checksum 指示詞,它會計算總和檢查碼並將值寫入 PDB 檔案。

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