常見的 C# 程式代碼慣例
程式代碼標準對於維護開發小組內的程式碼可讀性、一致性和共同作業至關重要。 遵循產業做法和已建立指導方針的程式代碼更容易瞭解、維護和擴充。 大部分的項目都會透過程式代碼慣例強制執行一致的樣式。 dotnet/docs
和 dotnet/samples
專案並不例外。 在此系列文章中,您將瞭解我們的編碼慣例,以及我們用來強制執行這些慣例的工具。 您可以依目前方式採用我們的慣例,或修改它們以符合小組的需求。
我們根據下列目標選擇我們的慣例:
- 正確性:我們的範例會複製並貼到您的應用程式中。 我們預期如此,因此我們需要讓程式代碼具有復原性和正確性,即使在多次編輯之後也是如此。
- 教學:我們的範例的目的是要教導所有 .NET 和 C#。 因此,我們不會對任何語言功能或 API 進行限制。 相反地,當特徵是不錯的選擇時,這些範例會教導。
- 一致性:讀者預期內容會有一致的體驗。 所有範例都應該符合相同的樣式。
- 採用:我們積極更新範例,以使用新的語言功能。 這種做法會提高對新功能的認識,並讓所有 C# 開發人員更熟悉這些功能。
重要
Microsoft 會使用這些指導方針來開發範例和檔。 它們是從 .NET 運行時間、C# 程式代碼撰寫樣式 和 C# 編譯程式 (roslyn) 指導方針中採用的。 我們選擇這些指導方針,因為它們已經過數年的開放原始碼開發測試。 他們已協助社群成員參與運行時間和編譯程序專案。 它們是常見 C# 慣例的範例,而不是權威清單(請參閱 該慣例的 架構設計指導方針)。
教學和採用目標是為什麼檔編碼慣例與運行時間和編譯程式慣例不同。 運行時間和編譯程式都有適用於經常性路徑的嚴格效能計量。 許多其他應用程式都不是。 我們的 教學 目標規定我們不會禁止任何建構。 相反地,範例會顯示應該使用建構的時機。 我們會比大部分生產應用程式更積極地更新範例。 我們的 採用 目標要求我們顯示您今天應該撰寫的程序代碼,即使去年撰寫的程式代碼不需要變更也一樣。
本文說明我們的指導方針。 指導方針隨著時間而演進,您會發現未遵循我們的指導方針的範例。 我們歡迎將這些範例帶入合規性的PR,或引起我們應更新之範例注意的問題。 我們的指導方針是開放原始碼,歡迎PR和問題。 不過,如果您的提交會變更這些建議,請先提出問題以供討論。 歡迎您使用我們的指導方針,或根據您的需求進行調整。
工具和分析器
工具可協助小組強制執行您的標準。 您可以啟用 程式代碼分析 來強制執行您偏好的規則。 您也可以建立 編輯器設定 ,讓 Visual Studio 自動強制執行您的樣式指導方針。 作為起點,您可以複製 dotnet/docs 存放庫的檔案 ,以使用我們的樣式。
這些工具可讓您的小組更輕鬆地採用您慣用的指導方針。 Visual Studio 會套用範圍中所有 .editorconfig
檔案中的規則,以格式化您的程序代碼。 您可以使用多個組態來強制執行全公司標準、小組標準,甚至是細微的項目標準。
違反啟用的規則時,程式代碼分析會產生警告和診斷。 您可以設定要套用至項目的規則。 然後,每個 CI 組建都會在開發人員違反任何規則時通知開發人員。
診斷識別碼
語言指導方針
下列各節說明 .NET 檔小組遵循的作法,以準備程序代碼範例和範例。 一般而言,請遵循下列做法:
- 盡可能利用新式語言功能和 C# 版本。
- 避免過時或過時的語言建構。
- 只攔截可以正確處理的例外狀況;避免攔截泛型例外狀況。
- 使用特定的例外狀況類型來提供有意義的錯誤訊息。
- 使用 LINQ 查詢和方法進行集合操作,以改善程式代碼可讀性。
- 使用異步程序設計搭配 async 和 await 進行 I/O 系結作業。
- 請謹慎處理死結,並在適當時使用 Task.ConfigureAwait 。
- 使用數據類型的語言關鍵詞,而不是運行時間類型。 例如,使用
string
而非, 或int
,而不是 System.Int32System.String。 - 使用
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;
- 使用 或
Action<>
委派所Func<>
定義的簽章呼叫 方法。
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 時,運算符會縮短。 也就是說,它不會評估第二個表達式。 運算子 & 會評估這兩者,在為 0 時 divisor
產生運行時錯誤。
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
更容易閱讀。注意
請小心不要不小心變更可反覆運算集合的元素類型。 例如,從 切換 System.Linq.IQueryable 至 System.Collections.IEnumerable
foreach
語句很容易,這會變更查詢的執行。
我們的一些範例會說明 表達式的自然類型 。 這些範例必須使用 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.AwesomeFeature.Azure
會比CoolStuff.Azure
對全域Azure
命名空間之前的新命名空間。 您可以將 修飾詞新增 global::
至 using
宣告來解決此問題。 不過,將宣告放在 using
命名空間之外會比較容易。
namespace CoolStuff.AwesomeFeature
{
using global::Azure;
public class Awesome
{
public void Stuff()
{
WaitUntil wait = WaitUntil.Completed;
// ...
}
}
}
樣式指導方針
一般而言,針對程式代碼範例使用下列格式:
- 使用四個空格進行縮排。 請勿使用索引標籤。
- 一致地對齊程式代碼以改善可讀性。
- 將行限制為 65 個字元,以增強檔上的程式代碼可讀性,特別是在行動畫面上。
- 將long語句分成多行,以改善清晰度。
- 使用大括弧的 「Allman」 樣式:開啟和右大括弧自己的新行。 大括弧會與目前的縮排層級對齊。
- 如有必要,換行符應該發生在二元運算符之前。
批注樣式
使用單行批注 (
//
) 進行簡短說明。請避免多行批注 (
/* */
) 以取得較長的說明。 批注不會當地語系化。 相反地,隨附文章中有較長的說明。用於描述方法、類別、欄位和所有公用成員都會使用 XML 批注。
將註解置於單獨的一行,不在程式碼行結尾處。
以大寫字母開始註解文字。
以句號結束註解文字。
在批注分隔符 (
//
) 和批註文字之間插入一個空格,如下列範例所示。// The following declaration creates a query. It does not run // the query.
版面配置慣例
好的版面配置使用格式設定,來強調程式碼的結構,並讓程式碼更易於閱讀。 Microsoft 範例遵循以下慣例:
使用預設程式碼編輯器設定 (智慧型縮排、四個字元縮排、定位點儲存為空格)。 如需詳細資訊,請參閱選項、文字編輯器、C#、格式。
每行只撰寫一個陳述式。
每行只撰寫一個宣告。
如果未自動縮排接續行,請縮排一個製表位(四個空格)。
在方法定義與屬性定義之間新增至少一個空白行。
使用括號清楚分隔運算式中的子句,如下列程式碼所示。
if ((startX > endX) && (startX > previousX)) { // Take appropriate action. }
例外狀況是範例說明運算符或表達式優先順序時。
安全性
請遵循安全程式碼撰寫方針中的指引。
意見反應
提交並檢視相關的意見反應