編寫更好的C#程式碼
引言
開發人員總是喜歡就編碼規範進行爭論,但更重要的是如何能夠在專案中自始至終地遵循編碼規範,以保證專案程式碼的一致性。並且團隊中的所有人都需要明確編碼規範所起到的作用。在這篇文章中,我會介紹一些在我多年的從業過程中所學習和總結的一些較好的實踐。
舉例為先
我們先來看一個 FizzBuzz 示例。FizzBuzz 要求編寫一個程式,遍歷從 1 到 100 的數字。其中如果某數字是 3 的倍數,則程式輸出 “Fizz”。如果某數字是 5 的倍數,則輸出 “Buzz”。如果某數字即是 3 的倍數也是 5 的倍數,則輸出 “FizzBuzz”。如果數字既不是 3 的倍數也不是 5 的倍數,則只需輸出該數字本身。
示例1:
public static void Test() { for (int i = 1; i < 101; i++) { if (i % 3 == 0 && i % 5 == 0) { Console.WriteLine("FizzBuzz"); } else if (i % 3 == 0) { Console.WriteLine("Fizz"); } else if (i % 5 == 0) { Console.WriteLine("Buzz"); } else { Console.WriteLine(i); } } }
什麼感覺?這段程式碼需要改進嗎?
示例2:
public static void Check() { for (int i = 1; i <= 100; i++) { string output = ""; if (i % 3 == 0) { output = "Fizz"; } if (i % 5 == 0) { output = output + "Buzz"; } if (output == "") { output = i.ToString(); } Console.WriteLine(output); } }
現在感覺如何?還能不能進一步改進?
好,讓我們來嘗試改進下。程式碼命名對所有軟體開發人員來說都是件非常困難的事情。我們花費了大量的時間來做這件事,而且有太多的需要被命名的元素,例如屬性、方法、類、檔案、專案等。不過我們的確需要花費一些精力在這些命名上,以使程式碼中的名稱更有意義,進而可以提高程式碼的可讀性。
public void DoFizzBuzz() { for (int number = 1; number <= 100; number++) { var output = GetFizzBuzzOutput(number); Console.WriteLine(output); } } private static string GetFizzBuzzOutput(int number) { string output = string.Empty; if (number % 3 == 0) { output = "Fizz"; } if (number % 5 == 0) { output += "Buzz"; } if (string.IsNullOrEmpty(output)) { output = number.ToString(); } return output; }
這次感覺怎樣?是不是比之前的示例要好些?是不是可讀性更好些?
什麼是更好的程式碼?
首先就是程式碼要為人來編寫,其次是為機器。從長期來看,編寫可讀性好的程式碼不會比編寫混亂的程式碼要花費更長的時間。如果你能夠非常容易地讀懂你寫的程式碼,那麼想確認其可以正常工作就更容易了。這應該已經是編寫易讀程式碼足夠充分的理由了。在很多情況下都需要閱讀程式碼,例如在程式碼評審中會閱讀你寫的程式碼,在你或者其他人修復Bug時會閱讀你寫的程式碼,在程式碼需要修改時也會讀到。還有就是當其他人準備在類似的專案或有類似功能的專案中嘗試複用你的部分程式碼時也會先閱讀你的程式碼。
“如果你只為你自己寫程式碼,為什麼要使程式碼更具可讀性?”
好,編寫易讀的程式碼最主要的原因是,在未來的一到兩週,你將工作在另一個專案上。而此時,有其他人需要修復當前專案的一個Bug,那麼將會發生什麼?我敢保證他肯定會迷失在你自己編寫的恐怖程式碼中。
從我的個人觀點來看,好的程式碼應該擁有以下幾個特徵:
- 程式碼容易編寫,並易於修改和擴充套件。
- 程式碼乾淨,並表述準確。
- 程式碼有價值,並注重質量。
所以,要時刻考慮先為人來編寫程式碼,然後再滿足機器的需要。
如何改進可讀性?
首先,你需要閱讀學習其他人編寫的程式碼,來了解什麼是好的程式碼,什麼是不好的程式碼。也就是那些你感覺非常容易理解的程式碼,和感覺看起來超級複雜的程式碼。然後,進行實踐。最後花費一些時間、經驗和實踐來改進你的程式碼的可讀性。一般來講僅通過培訓這種方式,在任何軟體公司中推動編碼規範都有些困難。而諸如結對程式碼評審,自動化程式碼評審工具等也可以幫助你。目前流行的工具有:
- FxCop:對 .NET 程式碼進行靜態程式碼分析,提供了多種規則來進行不同形式的分析。
- StyleCop:開源專案,其使用程式碼風格和一致性規範來對分析C#程式碼。可在 Visual Studio 中執行,也可以整合到 MSBuild 中。StyleCop 也已經被整合到了一些第三方開發工具中。
- JetBrains ReSharper:非常著名的提升生產力的工具,可以使 Microsoft Visual Studio IDE 更加強大。全世界的 .NET 開發人員可能都無法想象,工作中怎麼能沒有 ReSharper 的程式碼審查、程式碼自動重構、快速導航和編碼助手等這些強大的功能呢。
規範是什麼?
依據維基百科上的描述:"Coding conventions are a set of guidelines for a specific programming language that recommend programming style, practices and methods for each aspect of a piece program written in this language. These conventions usually cover file organization, indentation, comments, declarations, statements, white space, naming conventions, programming practices, programming principles, programming rules of thumb, architectural best practices, etc. These are guidelines for software structural quality. Software programmers are highly recommended to follow these guidelines to help improve the readability of their source code and make software maintenance easier. Coding conventions are only applicable to the human maintainers and peer reviewers of a software project. Conventions may be formalized in a documented set of rules that an entire team or company follows, or may be as informal as the habitual coding practices of an individual. Coding conventions are not enforced by compilers. As a result, not following some or all of the rules has no impact on the executable programs created from the source code."。
你應該能說出屬性、區域性變數、方法名、類名等的不同,因為它們使用不同的大小寫約定,所以這些約定非常有價值。通過網際網路,你已經瞭解了很多相應的準則和規範,你所需要的僅是找到一種規範或者建立你自己的規範,然後始終遵循該規範。
下面使用到的原始碼(類庫設計準則)是由微軟的 Special Interest Group 團隊開發的,我只是做了些擴充套件。
大小寫約定
下面是一些關於C#編碼標準、命名約定和最佳實踐的示例,可以根據你自己的需要來使用。
Pascal Casing
標示符中的首字母,後續串聯的每個單詞的首字母均為大寫。如果需要,標示符的前幾個字母均可大寫。
Camel Casing
標示符的首字母為小寫,後續串聯的每個單詞的首字母為大寫。
參考:標示符大小寫規則
一些命名約定示例
在網際網路上你可以找到足夠多的資源,我只是推薦幾個其中我最喜歡的:
這裡我展示了一些最基本的示例,但就像我上面已經提到的,找到一個適合你的規範,然後堅持使用。
要使用 Pascal Casing 為類和方法命名。
public class Product { public void GetActiveProducts() { //... } public void CalculateProductAdditinalCost() { //... } }
要使用 Camel Casing 為方法的引數和區域性變數命名。
public class ProductCategory { public void Save(ProductCategory productCategory) { // ... } }
不要使用縮寫語。
// Correct ProductCategory productCategory; // Avoid ProductCategory prodCat;
不要在標示符中使用下劃線。
// Correct ProductCategory productCategory; // Avoid ProductCategory product_Category;
要在介面名稱前使用字母 I 。
public interface IAddress { }
要在類的頂端定義所有成員變數,在最頂端定義靜態變數。
public class Product { public static string BrandName; public string Name { get; set; } public DateTime DateAvailable { get; set; } public Product() { // ... } }
要使用單數的詞彙定義列舉,除非是BitField列舉。
public enum Direction { North, East, South, West }
不要為列舉名稱新增Enum字尾。
//Avoid public enum DirectionEnum { North, East, South, West }
為什麼我們需要編碼規範?
在大型專案中,開發人員會常依賴於編碼規範。他們建立了很多規範和準則,以至於記住這些規範和準則已經變成了日常工作的一部分。計算機並不關心你寫的程式碼可讀性是否好,比起讀懂那些高階的程式語言語句,計算機更容易理解二進位制的機器指令。
編碼規範提供了很多明顯的好處,當然有可能你得到的更多。通常這些專案整體範圍的規劃,將使能夠將精力更多的集中在程式碼中更重要的部分上。
- 編碼規範可以幫助跨專案的傳遞知識。
- 編碼規範可以幫助你在新的專案上更快速的理解程式碼。
- 編碼規範強調組織中關聯專案間的關係。
你需要編寫可讀性高的程式碼,以此來幫助其他人來理解你的程式碼。程式碼命名對我們軟體開發人員來說是件非常困難的事情,我們在這上面已經花費了大量的時間,並且有太多的需要命名的元素,例如屬性、方法、類、檔案、專案等。所以我們確實需要花費一些精力在命名規範上,以使名稱更有意義,進而提高程式碼的可讀性。
還有,編碼規範可以讓你晚上睡得更香。
開發人員最應該遵循的幾個規則
始終控制類的大小
我曾經看到過,並且也曾寫過一些超大的類。而且不幸的是,結果總是不好的。後來我找到了真正原因,就是那些超大的類在嘗試做太多的事情,這違反了單一職責原則(SRP),也就是物件導向設計原則 SOLID 中的 S。
“The single responsibility principle states that every object should have a single responsibility, and that responsibility should be entirely encapsulated by the class. All its services should be narrowly aligned with that responsibility.”
或者按照 Martin Fowler 的定義:"THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE."
為什麼一定要將兩個職責分離到單獨的類中呢?因為每一個職責都是變化的中心。在需求變更時,這個變更將會出現在負責該職責的類中。如果一個類承擔了多個職責,就會有一個以上的原因導致其變化。如果一個類有多重職責,則說明這些職責已經耦合到了一起。並且某個職責的變化將有可能削弱或限制這個類滿足其他職責的能力。這種耦合將會導致非常脆弱的設計,進而在職責發生變化時,設計可能被意想不到的破壞了。
避免過時的註釋
先說什麼過時的註釋。按照 Robert C. Martin 的定義:
"A comment that has gotten old, irrelevant, and incorrect is obsolete. Comments get old quickly. It is best not to write a comment that will become obsolete. If you find an obsolete comment, it is best to update it or get rid of it as quickly as possible. Obsolete comments tend to migrate away from the code they once described. They become floating islands of irrelevance and misdirection in the code."
針對這個主題,不同水平的開發人員可能都會有自己的見解。我的建議是嘗試避免為單獨的方法或短小的類進行註釋。因為我所見過的大部分的註釋都是在嘗試描述程式碼的目的或意圖,或者某些註釋可能本身就沒什麼意義。通常開發人員通過寫註釋來提高程式碼的可讀性和可維護性,但要保證你所寫的註釋不會成為程式碼中的噪音。比起註釋,我認為合理的方法命名將更為有效,比如你可以為一個方法起一個更有意義的名字。大部分註釋都可能變成了無意義的程式碼噪音,讓我們來看看下面這些註釋:
//ensure that we are not exporting //deleted products if (product.IsDeleted && !product.IsExported) { ExportProducts = false; } // This is a for loop that prints the 1 million times for (int i = 0; i < 1000000; i++) { Console.WriteLine(i); }
如果我們不寫註釋,而是命名一個方法,比如叫 CancelExportForDeletedProducts() ,情況會怎樣?所以,合適的方法命名比註釋更有效。然而某些情況下,程式碼註釋也會非常有幫助,比如 Visual Studio 會從註釋生成 API 文件。此處的註釋略有不同,你需要使用 “///” 識別符號來註釋,這樣其他開發人員才能看到 API 或類庫的智慧提示。
我沒有說總是要避免註釋。按照 Kent Beck 說法,可以使用更多的註釋來描述程式整體是如何工作的,而不是對單獨的方法進行註釋。如果註釋是在嘗試描述程式碼的目的或意圖,那就錯了。如果你在程式碼中看到了密密麻麻的的註釋,你可能就會意識到有這麼多註釋說明程式碼寫的很糟糕。瞭解更多資訊可以閱讀下面這幾本書:
- 《Professional Refactoring in C# and ASP.NET》 by Danijel Arsenovski
- 《重構:改善既有程式碼設計》 by Martin Fowler, Kent Beck, John Brant, William Opdyke, Don Roberts
避免不必要的Region
Region 是 Visual Studio 提供的一個功能,它允許你將程式碼分塊。Region 的存在是因為它可以使大檔案導航變得容易。Region 還常被用於隱藏醜陋的程式碼,或者類已經膨脹的非常大了需要分塊。而如果一個類做了太多的事情,也就說明其違反了單一職責原則。所以,下次當你想新增一個 Region 時,先考慮下有沒有可能將這個 Region 分離到一個單獨的類中。
保持方法的短小
方法中的程式碼行數越多,則方法越難理解。我們推薦每個方法中只包含 20-25 行程式碼。但有些人說 1-10 行更合理,這只是些個人喜好,沒有硬性的規則。抽取方法是最常見的重構方式之一。如果你發現一個方法過長,或者已經需要一個註釋來描述它的目的了,那麼你就可以應用抽取方法了。人們總是會問一個方法到底多長合適,但其實長度並不是問題的根源。當你在處理複雜的方法時,跟蹤所有區域性變數是最複雜和消耗時間的,而通過抽取一個方法可以節省一些時間。可以使用 Visual Studio 來抽取方法,它會幫助你跟蹤區域性變數,並將其傳遞給新的方法或者接收方法的返回值。
Using ReSharper
Using Microsoft Visual Studio
更多的資訊可以參考 MSDN。
按照《重構:改善既有程式碼設計》中的描述,
"Extract Method is one of the most common refactoring I do. I look at a method that is too long or look at code that needs a comment to understand its purpose. I then turn that fragment of code into its own method. I prefer short, well-named methods for several reasons. First, it increases the chances that other methods can use a method when the method is finely grained. Second, it allows the higher-level methods to read more like a series of comments. Overriding also is easier when the methods are finely grained. It does take a little getting used to if you are used to seeing larger methods. And small methods really work only when you have good names, so you need to pay attention to naming. People sometimes ask me what length I look for in a method. To me length is not the issue. The key is the semantic distance between the method name and the method body. If extracting improves clarity, do it, even if the name is longer than the code you have extracted."
避免過多的引數
通過宣告一個類來代替多個引數。建立一個類,用於包含所有的引數。通常來講,這是一個較好的設計,並且這個抽象非常的有價值。
//avoid public void Checkout(string shippingName, string shippingCity, string shippingSate, string shippingZip, string billingName, string billingCity, string billingSate, string billingZip) { } //DO public void Checkout(ShippingAddress shippingAddress, BillingAddress billingAddress) { }
我們需要引入類來代替所有的引數。
避免複雜的表示式
if(product.Price>500 && !product.IsDeleted && !product.IsFeatured && product.IsExported) { // do something }
複雜的表示式意味著其背後隱藏了一些涵義,我們可以通過使用屬性來封裝這些表示式,進而使程式碼更易讀些。
把警告等同於錯誤
如果你注意看程式碼,你會發現一個變數被宣告瞭但從沒被使用過。正常來講,我們編譯工程後會得到一個警告,但仍可以執行工程而不會發生任何錯誤。但是我們應該儘可能地移除這些警告。通過如下步驟可以在工程上設定將警告等同於錯誤:
精簡多處返回
在每段程式中都減少函式返回的數量。假設從底部開始閱讀程式碼,你很難意識到有可能在上面的某處已經返回了,這樣的程式碼將是非常難理解的。
僅使用一處返回可以增強可讀性。如果程式這麼寫的話可能看起來比較乾淨,但不立即返回也意味著需要編寫更多程式碼。
//avoid if(product.Price>15) { return false; } else if(product.IsDeleted) { return false; } else if(!product.IsFeatured) { return false; } else if() { //..... } return true;
//DO var isValid = true; if(product.Price>15) { isValid= false; } else if(product.IsDeleted) { isValid= false; } else if(!product.IsFeatured) { isValid= false; } return isValid;
你可以想象在這 20-30 行程式碼中就散落了 4 個退出點,這會使你非常難理解到底程式內部做了什麼,到底會執行什麼,什麼時候執行。
關於這一點我得到了很多人的回覆,一些人同意這個觀點,有些則不同意這是一個好的編碼標準。為了找出潛在的問題,我做了些單元測試,發現如果複雜的方法包含多個退出點,通常情況下會需要一組測試來覆蓋所有的路徑。
if( BADFunction() == true) { // expression if( anotherFunction() == true ) { // expression return true; } else { //error } } else { //error } return false;
if( !GoodFunction()) { // error. return false } // expression if( !GoodFunction2()) { //error. return false; } // more expression return true;
進一步理解可以參考 Steve McConnell 的《程式碼大全》。
使用斷言
在軟體開發中,斷言程式碼常被用於檢查程式程式碼是否按照其設計在執行。通常 True 代表所有操作按照預期的完成,False 代表已經偵測到了一些意外的錯誤。斷言通常會接收兩個引數,一個布林型的表示式用於一個描述假設為真的假定,一個訊息引數用於描述斷言失敗的原因。
尤其在開發大型的、複雜的高可靠系統中,斷言通常是非常有用的功能。
例如:如果系統假設將最多支援 100,000 使用者記錄,系統中可能會包含一個斷言來檢查使用者記錄數小於等於 100,000,在這種範圍下,斷言不會起作用。但如果使用者記錄數量超過了 100,000,則斷言將會丟擲一個錯誤來告訴你記錄數已經超出了範圍。
檢查迴圈端點值
一個迴圈通常會涉及三種條件值:第一個值、中間的某值和最後一個值。但如果你有任何其他的特定條件,也需要進行檢測。如果迴圈中包含了複雜的計算,請不要使用計算器,要手工檢查計算結果。
總結
通常在任何軟體公司中推行編碼規範都需要按照組織行為、專案屬性和領域來進行,在此我想再次強調“找到一個適合你的編碼規範,並一直遵循它”。
如果你認為我遺漏了某個特別有用的編碼準則,請在評論中描述,我會嘗試補充到文章中。
Coding For Fun.
相關文章
- 編寫更好的CSS程式碼CSS
- 用jQuery編寫出更好的程式碼jQuery
- 編寫更好程式碼的 6 個提示
- 編寫更好的jQuery程式碼的建議jQuery
- 編寫更好的CSSCSS
- 編寫更好程式碼的6個提示【已翻譯100%】
- 如何寫出更好的 React 程式碼?React
- 如何寫出更好的Java程式碼Java
- 如何編寫高質量的C#程式碼(一)C#
- 編寫更好 Bash 指令碼的 8 個建議指令碼
- 【譯】如何寫出更好的 React 程式碼React
- [譯] 如何寫出更好的 React 程式碼?React
- 編寫高效能C#程式碼 —— Span<T>C#
- 【譯】如何更好的編寫CSSCSS
- Vue3,用組合的方式來編寫更好的程式碼(1/5)Vue
- 程式設計師如何寫出更好的程式碼程式設計師
- c#學習之--編寫程式碼相關的問題C#
- Vue3,用組合編寫更好的程式碼:動態返回(3/4)Vue
- 如何更好的編寫async函式函式
- C# Windows Service 服務程式的編寫C#Windows
- Vue3,用組合編寫更好的程式碼:靈活的引數(2/5)Vue
- 遠離你的電腦,程式碼寫得更好!
- 5款工具助你寫出更好的Java程式碼Java
- 編寫可讀的程式碼
- Vue3,用組合編寫更好的程式碼:Async Without Await 模式(4/4)VueAI模式
- 30 多年的軟體經驗,總結出 10 個編寫出更好程式碼的技巧
- C# 飛機票購買程式編寫C#
- Sublime 編寫編譯 swift程式碼編譯Swift
- 使用正規表示式編寫更好的 SQLSQL
- 使用正規表示式編寫更好的SQLSQL
- 如何編寫簡潔的程式碼?
- 編寫易於理解的程式碼
- 編寫程式碼的好習慣
- 編寫高效的Android程式碼Android
- 編寫易讀的程式碼 (轉)
- 程式碼編寫提示配置
- 如何編寫MapReduce程式碼
- 讀《程式碼不朽:編寫可維護軟體的10大要則》C# 版C#