常用的 C# 程式碼慣例
編碼慣例對於在開發小組內保持程式碼可讀性、一致性和共同作業來說至關重要。 遵循產業做法和所制定方針的程式碼會更為容易理解、維護和擴充。 大部分的專案都會透過程式碼慣例強制執行一致的樣式。 dotnet/docs
和 dotnet/samples
專案也不例外。 在這一系列文章中,您會了解我們的編碼慣例,以及我們用來強制執行這些慣例的工具。 您可以原封不動地採用我們的慣例,也可加以修改使其符合您小組的需求。
我們根據下列目標選擇了我們的慣例:
- 正確性:我們的範例會複製並貼到您的應用程式中。 我們希望您這麼做,因此我們需要讓程式碼具有復原性和正確性,即使經過多次編輯之後也是如此。
- 教學:我們的範例是為了進行 .NET 和 C# 的所有教學。 因此,我們不會對任何語言功能或 API 施加限制。 相反地,這些範例會教導讀者某個功能何時會是不錯的選擇。
- 一致性:讀者期望能在我們的內容中享有一致的體驗。 所有範例都應該符合相同的樣式。
- 採用:我們會積極更新範例,以使用新的語言功能。 這種做法可加深讀者對新功能的認識,並讓所有 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-catch
和 using
陳述式
針對大部分例外狀況處理,請使用 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,但沒有在結果在將它們保留為
Name
和ID
,請對它們重新命名,以釐清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.Azure
或 CoolStuff.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. }
範例說明的是運算子或運算式優先順序時則例外。
安全性
請遵循安全程式碼撰寫方針中的指引。