共用方式為


常用的 C# 程式碼慣例

編碼慣例對於在開發小組內保持程式碼可讀性、一致性和共同作業來說至關重要。 遵循產業做法和所制定方針的程式碼會更為容易理解、維護和擴充。 大部分的專案都會透過程式碼慣例強制執行一致的樣式。 dotnet/docsdotnet/samples 專案也不例外。 在這一系列文章中,您會了解我們的編碼慣例,以及我們用來強制執行這些慣例的工具。 您可以原封不動地採用我們的慣例,也可加以修改使其符合您小組的需求。

我們根據下列目標選擇了我們的慣例:

  1. 正確性:我們的範例會複製並貼到您的應用程式中。 我們希望您這麼做,因此我們需要讓程式碼具有復原性和正確性,即使經過多次編輯之後也是如此。
  2. 教學:我們的範例是為了進行 .NET 和 C# 的所有教學。 因此,我們不會對任何語言功能或 API 施加限制。 相反地,這些範例會教導讀者某個功能何時會是不錯的選擇。
  3. 一致性:讀者期望能在我們的內容中享有一致的體驗。 所有範例都應該符合相同的樣式。
  4. 採用:我們會積極更新範例,以使用新的語言功能。 這種做法可加深讀者對新功能的認識,並讓所有 C# 開發人員更熟悉這些新功能。

重要

Microsoft 會使用這些方針來開發範例與文件。 這些方針採用自 .NET 執行階段、C# 編碼樣式C# 編譯器 (roslyn) 方針。 我們之所以選擇這些指導方針,是因為他們在數年的開放原始碼開發中採用。 這些指導方針可協助社群成員參與運行時間和編譯程序專案。 它們是常見 C# 慣例的範例,而不是權威清單(如需詳細指導方針,請參閱 架構設計指導方針)。

「教學」和「採用」目標是造成文件編碼慣例與執行階段和編譯器慣例不同的原因。 執行階段和編譯器在最忙碌路徑這一點上都有嚴格的效能計量。 許多其他應用程式則沒有。 我們的「教學」目標使得我們不會禁止任何建構。 相反地,範例必須顯示何時應該使用建構。 我們更新範例的積極程度會高過大多數的生產應用程式。 我們的「採用」目標使得我們必須顯示您今天應該撰寫的程式碼,即使去年撰寫的程式碼不需要變更也一樣。

本文說明我們的方針。 指導方針會隨著時間而演進,您會發現未遵循我們的指導方針的範例。 歡迎您提出可讓這些範例變得合規的 PR,或回報問題以讓我們注意到應該更新的範例。 我們的方針採開源方式,歡迎您提出 PR 和回報問題。 不過,如果您的提交會導致這些建議發生改變,請先提問以便進行討論。 歡迎您使用我們的方針,或是加以調整以符合您的需求。

工具和分析器

工具可協助小組強制執行您的慣例。 您可以啟用程式碼分析以強制執行您想要的規則。 您也可以建立 editorconfig,讓 Visual Studio 自動強制執行您的樣式方針。 作為起點,您可以複製 dotnet/docs.editorconfig,以使用我們的樣式。

這些工具可讓您的小組更輕鬆地採用您想要的方針。 Visual Studio 會將範圍內所有 .editorconfig 檔案中的規則套用於格式化您的程式碼。 您可以使用多個組態來強制執行全公司的慣例、小組慣例,甚至是細微的專案慣例。

程序代碼分析會在偵測規則違規時產生警告和診斷。 請設定要套用至專案的規則。 然後,每個 CI 組建便會在開發人員違反任何規則時,向開發人員發出通知。

診斷識別碼

語言指導方針

下列各節說明 .NET 小組在準備程式碼範例時應遵循的做法。 一般而言,請遵循下列做法:

  • 盡可能地利用新式語言功能和 C# 版本。
  • 避免過時的語言建構。
  • 只捕捉可以正確處理的例外狀況;避免捕捉一般的例外狀況。 例如,範例程式代碼不應該在沒有例外狀況篩選的情況下攔截 System.Exception 類型。
  • 使用具體的例外狀況類型來提供有意義的錯誤訊息。
  • 使用 LINQ 查詢和方法進行集合操作,以改善程式碼可讀性。
  • 搭配使用非同步程式設計與 async 和 await 以進行與 I/O 繫結的作業。
  • 請謹慎處理死結,並在適當時使用 Task.ConfigureAwait
  • 使用資料類型 (而非執行階段類型) 的語言關鍵字。 例如,使用 string 而非 System.String,或使用 int 而非 System.Int32。 此建議包括使用類型 nintnuint
  • 使用 int 而非不帶正負號的類型。 int 的使用在 C# 中非常普遍,當您使用 int 時,可更易於與其他程式庫互動。 例外狀況適用於不帶正負號資料類型的特定文件。
  • 只在讀者可以從運算式推斷類型時,才使用 var。 讀者會在文件平台上檢視我們的範例。 其沒有可顯示變數類型的暫留或工具提示。
  • 以清楚、簡單的方式撰寫程式碼。
  • 避免太過錯綜複雜的程式碼邏輯。

下面有更具體的方針。

字串資料

  • 使用字串內插補點串連短字串,如下列程式碼所示。

    string displayName = $"{nameList[n].LastName}, {nameList[n].FirstName}";
    
  • 若要在迴圈中附加字串 (特別是正在使用大量文字時),請使用 System.Text.StringBuilder 物件。

    var phrase = "lalalalalalalalalalalalalalalalalalalalalalalalalalalalalala";
    var manyPhrases = new StringBuilder();
    for (var i = 0; i < 10000; i++)
    {
        manyPhrases.Append(phrase);
    }
    //Console.WriteLine("tra" + manyPhrases);
    
  • 偏好使用原始字串常數而非逸出序列或逐字字串。

    var message = """
        This is a long message that spans across multiple lines.
        It uses raw string literals. This means we can 
        also include characters like \n and \t without escaping them.
        """;
    
  • 使用以表達式為基礎的字串插補,而不是位置字串插補。

    // Execute the queries.
    Console.WriteLine("scoreQuery:");
    foreach (var student in scoreQuery)
    {
        Console.WriteLine($"{student.Last} Score: {student.score}");
    }
    

建構函式和初始化

  • 針對記錄類型的主要建構函式參數使用Pascal案例:

    public record Person(string FirstName, string LastName);
    
  • 使用駝峰式大小寫為類別和結構類型的主要建構函式參數命名。

  • 使用 required 屬性,而不是建構函式來強制初始化屬性值:

    public class LabelledContainer<T>(string label)
    {
        public string Label { get; } = label;
        public required T Contents 
        { 
            get;
            init;
        }
    }
    

陣列和集合

  • 使用集合表示式初始化所有集合類型:
string[] vowels = [ "a", "e", "i", "o", "u" ];

委派

  • 使用 Func<>Action<>,而不是定義委派類型。 在類別中,定義委派方法。
Action<string> actionExample1 = x => Console.WriteLine($"x is: {x}");

Action<string, string> actionExample2 = (x, y) =>
    Console.WriteLine($"x is: {x}, y is {y}");

Func<string, int> funcExample1 = x => Convert.ToInt32(x);

Func<int, int, int> funcExample2 = (x, y) => x + y;
  • 使用 Func<>Action<> 委派所定義的簽章來呼叫方法。
actionExample1("string for x");

actionExample2("string for x", "string for y");

Console.WriteLine($"The value is {funcExample1("1")}");

Console.WriteLine($"The sum is {funcExample2(1, 2)}");
  • 如果您建立委派類型執行個體,則可以使用簡潔的語法。 在類別中,定義委派類型以及具有相符簽章的方法。

    public delegate void Del(string message);
    
    public static void DelMethod(string str)
    {
        Console.WriteLine("DelMethod argument: {0}", str);
    }
    
  • 建立委派類型執行個體,並對其進行呼叫。 下列宣告會顯示精簡語法。

    Del exampleDel2 = DelMethod;
    exampleDel2("Hey");
    
  • 下列宣告會使用完整語法。

    Del exampleDel1 = new Del(DelMethod);
    exampleDel1("Hey");
    

例外狀況處理中的 try-catchusing 陳述式

  • 針對大部分例外狀況處理,請使用 try-catch 陳述式。

    static double ComputeDistance(double x1, double y1, double x2, double y2)
    {
        try
        {
            return Math.Sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
        }
        catch (System.ArithmeticException ex)
        {
            Console.WriteLine($"Arithmetic overflow or underflow: {ex}");
            throw;
        }
    }
    
  • 使用 C# using 陳述式,可簡化程式碼。 如果您有 try-finally 陳述式,而其中 finally 區塊內唯一的程式碼是 Dispose 方法的呼叫,則請改用 using 陳述式。

    在下列範例中,try-finally 陳述式只會呼叫 Dispose 區塊中的 finally

    Font bodyStyle = new Font("Arial", 10.0f);
    try
    {
        byte charset = bodyStyle.GdiCharSet;
    }
    finally
    {
        bodyStyle?.Dispose();
    }
    

    您可以使用 using 陳述式來執行相同的動作。

    using (Font arial = new Font("Arial", 10.0f))
    {
        byte charset2 = arial.GdiCharSet;
    }
    

    使用不需要大括弧的新 using 語法

    using Font normalStyle = new Font("Arial", 10.0f);
    byte charset3 = normalStyle.GdiCharSet;
    

&&|| 運算子

  • 在執行比較時,請使用 && 而非 &,使用 || 而非 |,如下列範例所示。

    Console.Write("Enter a dividend: ");
    int dividend = Convert.ToInt32(Console.ReadLine());
    
    Console.Write("Enter a divisor: ");
    int divisor = Convert.ToInt32(Console.ReadLine());
    
    if ((divisor != 0) && (dividend / divisor) is var result)
    {
        Console.WriteLine("Quotient: {0}", result);
    }
    else
    {
        Console.WriteLine("Attempted division by 0 ends up here.");
    }
    

如果除數為 0,則 if 陳述式中的第二個子句將會造成執行階段錯誤。 但是,第一個運算式為 false 時,&& 運算子會縮短。 即,不會評估第二個運算式。 divisor 為 0 時,& 運算子將會評估這兩者,因而導致執行階段錯誤。

new 運算子

  • 當變數類型符合物件類型時,請使用其中一種簡潔的物件具現化形式,如下列宣告所示。 當變數是介面類型或運行時間類型的基類時,此表單無效。

    var firstExample = new ExampleClass();
    
    ExampleClass instance2 = new();
    

    上述宣告相當於下列宣告。

    ExampleClass secondExample = new ExampleClass();
    
  • 使用物件初始設定式來簡化物件建立,如下列範例中所示。

    var thirdExample = new ExampleClass { Name = "Desktop", ID = 37414,
        Location = "Redmond", Age = 2.3 };
    

    下列範例會設定與上述範例相同的屬性,但不會使用初始設定式。

    var fourthExample = new ExampleClass();
    fourthExample.Name = "Desktop";
    fourthExample.ID = 37414;
    fourthExample.Location = "Redmond";
    fourthExample.Age = 2.3;
    

事件處理

  • 請使用 Lambda 運算式來定義稍後不需要移除的事件處理常式:
public Form2()
{
    this.Click += (s, e) =>
        {
            MessageBox.Show(
                ((MouseEventArgs)e).Location.ToString());
        };
}

Lambda 運算式會縮短下列傳統定義。

public Form1()
{
    this.Click += new EventHandler(Form1_Click);
}

void Form1_Click(object? sender, EventArgs e)
{
    MessageBox.Show(((MouseEventArgs)e).Location.ToString());
}

靜態成員

使用類別名稱 ClassName.StaticMember,呼叫 static 成員。 這種作法可讓靜態存取更加清晰,從而讓程式碼更易於閱讀。 請不要使用衍生類別名稱,來限定基底類別中所定義的靜態成員。 當該程式代碼編譯時,程式代碼可讀性會誤導,而且如果您將具有相同名稱的靜態成員新增至衍生類別,程式代碼可能會在未來中斷。

LINQ 查詢

  • 請為查詢變數使用有意義的名稱。 下列範例對位於西雅圖的客戶,使用 seattleCustomers

    var seattleCustomers = from customer in Customers
                           where customer.City == "Seattle"
                           select customer.Name;
    
  • 使用別名,確保匿名類型的屬性名稱正確使用 Pascal 大小寫慣例。

    var localDistributors =
        from customer in Customers
        join distributor in Distributors on customer.City equals distributor.City
        select new { Customer = customer, Distributor = distributor };
    
  • 當結果中的屬性名稱可能會造成混淆時,請重新命名屬性。 例如,如果您的查詢傳回客戶名稱和散發者名稱,而不是將它們保留為結果中的 Name 形式,請重新命名它們以釐清 CustomerName 是客戶的名稱,而 DistributorName 是散發者的名稱。

    var localDistributors2 =
        from customer in Customers
        join distributor in Distributors on customer.City equals distributor.City
        select new { CustomerName = customer.Name, DistributorName = distributor.Name };
    
  • 在查詢變數和範圍變數的宣告中使用隱含類型。 這個關於 LINQ 查詢中隱含型別的指導會覆寫隱含型別區域變數的一般規則。 LINQ 查詢通常使用會建立匿名型別的投影。 其他查詢運算式則會建立具有巢狀泛型型別的結果。 隱含型別變數通常有更好的可讀性。

    var seattleCustomers = from customer in Customers
                           where customer.City == "Seattle"
                           select customer.Name;
    
  • from 子句下方對齊查詢子句,如先前範例中所示。

  • 在其他查詢子句前面使用 where 子句,以確保之後的查詢子句會針對已縮減並篩選過的一組資料進行操作。

    var seattleCustomers2 = from customer in Customers
                            where customer.City == "Seattle"
                            orderby customer.Name
                            select customer;
    
  • 請使用多個 from 子句來存取內部集合,而非使用 join 子句。 例如,Student 物件的集合可能每一個都包含測驗分數的集合。 執行下列查詢時,它會傳回每個超過90分的分數,以及收到分數的學生姓氏。

    var scoreQuery = from student in students
                     from score in student.Scores
                     where score > 90
                     select new { Last = student.LastName, score };
    

隱含型別區域變數

  • 如果區域變數的型別明顯來自指派的右側,請針對該變數使用隱含型別

    var message = "This is clearly a string.";
    var currentTemperature = 27;
    
  • 型別不是明顯來自指派的右側時,請不要使用 var。 請不要假設類型明確來自方法名稱。 變數類型若是 new 運算符、明確轉換或指派給字面值,則會被視為清楚。

    int numberOfIterations = Convert.ToInt32(Console.ReadLine());
    int currentMaximum = ExampleClass.ResultSoFar();
    
  • 請勿使用變數名稱來指定變數的型別。 有可能會不正確。 請改用型別來指定型別,並使用變數名稱來指出變數的語意資訊。 下列範例應該使用 string 來指定型別,並使用 iterations 之類的名稱來指出從主控台讀取的資訊所代表的意義。

    var inputInt = Console.ReadLine();
    Console.WriteLine(inputInt);
    
  • 避免使用 var 取代 dynamic。 當您想要執行階段類型推斷時,請使用 dynamic。 如需詳細資訊,請參閱使用類型動態 (C# 程式設計指南)

  • for 迴圈中針對迴圈變數使用隱含型別。

    下列範例在 for 陳述式中使用隱含類型。

    var phrase = "lalalalalalalalalalalalalalalalalalalalalalalalalalalalalala";
    var manyPhrases = new StringBuilder();
    for (var i = 0; i < 10000; i++)
    {
        manyPhrases.Append(phrase);
    }
    //Console.WriteLine("tra" + manyPhrases);
    
  • 請不要使用隱含類型來判定 foreach 迴圈中迴圈變數的類型。 在大部分情況下,集合中元素的類型並不明顯。 集合的名稱不應該只依賴推斷其元素的類型。

    下列範例在 foreach 陳述式中使用明確類型。

    foreach (char ch in laugh)
    {
        if (ch == 'h')
        {
            Console.Write("H");
        }
        else
        {
            Console.Write(ch);
        }
    }
    Console.WriteLine();
    
  • 針對 LINQ 查詢中的結果序列使用隱含型別。 關於 LINQ 的章節說明許多 LINQ 查詢會導致必須使用隱含型別的匿名型別。 其他查詢則會導致巢狀泛型型別,此時 var 會有更好的可讀性。

    注意

    請小心不要意外變更可反覆運算集合的元素類型。 例如,在 foreach 語句中,將 System.Linq.IQueryable 切換至 System.Collections.IEnumerable 很容易,這會改變查詢的執行。

我們的一些範例會說明運算式的自然型別。 這些範例必須使用 var,以便讓編譯器挑選自然型別。 即使這些範例較不明顯,但範例中必須使用 var。 文字應該要說明此行為。

檔案範圍命名空間宣告

大部分的程式代碼檔案都會宣告單一命名空間。 因此,我們的範例應該使用檔案範圍命名空間宣告:

namespace MySampleCode;

將 using 指示詞放在命名空間宣告外部

using 指示詞在命名空間宣告外部時,該匯入的命名空間就是其完整名稱。 完整名稱會比較清楚。 當 using 指示詞位於命名空間內時,其可以是相對於該命名空間的名稱,也可以是其完整名稱。

using Azure;

namespace CoolStuff.AwesomeFeature
{
    public class Awesome
    {
        public void Stuff()
        {
            WaitUntil wait = WaitUntil.Completed;
            // ...
        }
    }
}

假設 WaitUntil 類別有參考 (直接或間接)。

現在,請略微進行變更:

namespace CoolStuff.AwesomeFeature
{
    using Azure;

    public class Awesome
    {
        public void Stuff()
        {
            WaitUntil wait = WaitUntil.Completed;
            // ...
        }
    }
}

並且現在進行編譯。 連同明天。 但之後 (有時是下週),上述 (未碰過的) 程式碼會失敗,並出現兩個錯誤:

- error CS0246: The type or namespace name 'WaitUntil' could not be found (are you missing a using directive or an assembly reference?)
- error CS0103: The name 'WaitUntil' does not exist in the current context

命名空間中引入此類別的一個相依性,然後以 .Azure結尾:

namespace CoolStuff.Azure
{
    public class SecretsManagement
    {
        public string FetchFromKeyVault(string vaultId, string secretId) { return null; }
    }
}

放置在命名空間內的 using 指示詞會區分內容,並讓名稱解析變得複雜。 在此範例中,這是找到的第一個命名空間。

  • CoolStuff.AwesomeFeature.Azure
  • CoolStuff.Azure
  • Azure

新增符合 CoolStuff.AzureCoolStuff.AwesomeFeature.Azure 的新命名空間,將會在全域 Azure 命名空間之前進行比對。 您可以將 global:: 修飾元新增至 using 宣告來解決此問題。 不過,改為將 using 宣告放在命名空間外部會比較容易。

namespace CoolStuff.AwesomeFeature
{
    using global::Azure;

    public class Awesome
    {
        public void Stuff()
        {
            WaitUntil wait = WaitUntil.Completed;
            // ...
        }
    }
}

樣式方針

一般而言,請針對程式碼範例使用下列格式:

  • 使用四個空格來縮排。 請勿使用定位字元。
  • 一致地對齊程式碼以改善可讀性。
  • 將每行限制為 65 個字元,以增強文件上程式碼的可讀性,特別是在手機畫面上。
  • 藉由將長語句分成多行來改善清楚度和用戶體驗。
  • 針對大括弧使用「Allman」樣式:左右大括弧各自在新的一行。 大括弧與目前的縮排層級對齊。
  • 如有必要,換行符號應出現在二元運算子之前。

註解樣式

  • 使用單行註解 (//) 來進行簡短說明。

  • 避免使用多行註解 (/* */) 來進行較長的說明。
    程式碼範例中的註解不會當地語系化。 這表示不會翻譯內嵌在程序代碼中的說明。 較長的說明文字應該放在隨附文章中,以便進行當地語系化。

  • 若要描述方法、類別、欄位和所有公用成員,請使用 XML 註解

  • 將註解置於單獨的一行,不在程式碼行結尾處。

  • 以大寫字母開始註解文字。

  • 以句號結束註解文字。

  • 註解分隔符號 (//) 與註解文字之間插入一個空格,如下列範例所示。

    // The following declaration creates a query. It does not run
    // the query.
    

版面配置慣例

好的版面配置使用格式設定,來強調程式碼的結構,並讓程式碼更易於閱讀。 Microsoft 範例遵循以下慣例:

  • 使用預設程式碼編輯器設定 (智慧型縮排、四個字元縮排、定位點儲存為空格)。 如需詳細資訊,請參閱選項、文字編輯器、C#、格式

  • 每行只撰寫一個陳述式。

  • 每行只撰寫一個宣告。

  • 如果連續行不會自動縮排,則縮排一個定位停駐點 (四個空格)。

  • 在方法定義與屬性定義之間新增至少一個空白行。

  • 使用括號清楚分隔運算式中的子句,如下列程式碼所示。

    if ((startX > endX) && (startX > previousX))
    {
        // Take appropriate action.
    }
    

範例說明的是運算子或運算式優先順序時則例外。

安全性

請遵循安全程式碼撰寫方針中的指引。