常用的 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 存放庫的檔案,以使用我們的樣式。

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

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

診斷識別碼

語言指導方針

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

  • 盡可能地利用新式語言功能和 C# 版本。
  • 避免已淘汰或過時的語言建構。
  • 只攔截可以正確處理的例外狀況;避免攔截泛型例外狀況。
  • 使用具體的例外狀況類型來提供有意義的錯誤訊息。
  • 使用 LINQ 查詢和方法進行集合操作,以改善程式碼可讀性。
  • 搭配使用非同步程式設計與 async 和 await 以進行與 I/O 繫結的作業。
  • 請謹慎處理死結,並在適當時使用 Task.ConfigureAwait
  • 使用資料類型 (而非執行階段類型) 的語言關鍵字。 例如,使用 string 而非 System.String,或使用 int 而非 System.Int32
  • 使用 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,而是該使用 string[]
string[] vowels1 = { "a", "e", "i", "o", "u" };
  • 如果您使用明確具現化,則可以使用 var
var vowels2 = new string[] { "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 陳述式只會呼叫 finally 區塊中的 Dispose

    Font bodyStyle = new Font("Arial", 10.0f);
    try
    {
        byte charset = bodyStyle.GdiCharSet;
    }
    finally
    {
        if (bodyStyle != null)
        {
            ((IDisposable)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 };
    
  • 當結果中的屬性名稱可能會造成混淆時,請重新命名屬性。 例如,如果查詢傳回了客戶名稱與經銷商 ID,但沒有在結果在將它們保留為 NameID,請對它們重新命名,以釐清 Name 是客戶的名稱,而 ID 是經銷商的 ID。

    var localDistributors2 =
        from customer in customers
        join distributor in distributors on customer.City equals distributor.City
        select new { CustomerName = customer.Name, DistributorID = distributor.ID };
    
  • 在查詢變數和範圍變數的宣告中使用隱含類型。 這個關於 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。 文字應該要說明此行為。

將 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.
    }
    

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

安全性

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