編寫更好的C#代碼

引言

  開發人員老是喜歡就編碼規範進行爭論,但更重要的是如何可以在項目中自始至終地遵循編碼規範,以保證項目代碼的一致性。而且團隊中的全部人都須要明確編碼規範所起到的做用。在這篇文章中,我會介紹一些在我多年的從業過程當中所學習和總結的一些較好的實踐。express

 舉例爲先

  咱們先來看一個 FizzBuzz 示例。FizzBuzz 要求編寫一個程序,遍歷從 1 到 100 的數字。其中若是某數字是 3 的倍數,則程序輸出 「Fizz」。若是某數字是 5 的倍數,則輸出 「Buzz」。若是某數字便是 3 的倍數也是 5 的倍數,則輸出 「FizzBuzz」。若是數字既不是 3 的倍數也不是 5 的倍數,則只需輸出該數字自己。app

  示例1:ide

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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:工具

1
2
3
4
5
6
7
8
9
10
11
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);
       }
}

  如今感受如何?還能不能進一步改進?oop

  好,讓咱們來嘗試改進下。代碼命名對全部軟件開發人員來講都是件很是困難的事情。咱們花費了大量的時間來作這件事,並且有太多的須要被命名的元素,例如屬性、方法、類、文件、項目等。不過咱們的確須要花費一些精力在這些命名上,以使代碼中的名稱更有意義,進而能夠提升代碼的可讀性。單元測試

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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 爲類和方法命名。

1
2
3
4
5
6
7
8
9
10
11
public class Product
{
       public void GetActiveProducts()
       {
         //...
       }
       public void CalculateProductAdditinalCost()
       {
         //...
       }
}

  要使用 Camel Casing 爲方法的參數和局部變量命名。

1
2
3
4
5
6
7
public class ProductCategory
{
       public void Save(ProductCategory productCategory)
       {
         // ...
       }
}

  不要使用縮寫語。

1
2
3
4
5
// Correct
ProductCategory productCategory;
 
// Avoid
ProductCategory prodCat;

  不要在標示符中使用下劃線。

1
2
3
4
5
// Correct
ProductCategory productCategory;
 
// Avoid
ProductCategory product_Category;

  要在接口名稱前使用字母 I 。

1
2
3
public interface IAddress
{
}

  要在類的頂端定義全部成員變量,在最頂端定義靜態變量。

1
2
3
4
5
6
7
8
9
10
11
12
public class Product
{
     public static string BrandName;
 
     public string Name { get ; set ; }
     public DateTime DateAvailable { get ; set ; }
 
     public Product()
     {
       // ...
     }
}

  要使用單數的詞彙定義枚舉,除非是BitField枚舉。

1
2
3
4
5
6
7
public enum Direction
{
     North,
     East,
     South,
     West
}

  不要爲枚舉名稱添加Enum後綴。

1
2
3
4
5
6
7
8
//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."

  針對這個主題,不一樣水平的開發人員可能都會有本身的看法。個人建議是嘗試避免爲單獨的方法或短小的類進行註釋。由於我所見過的大部分的註釋都是在嘗試描述代碼的目的或意圖,或者某些註釋可能自己就沒什麼意義。一般開發人員經過寫註釋來提升代碼的可讀性和可維護性,但要保證你所寫的註釋不會成爲代碼中的噪音。比起註釋,我認爲合理的方法命名將更爲有效,好比你能夠爲一個方法起一個更有意義的名字。大部分註釋均可能變成了無心義的代碼噪音,讓咱們來看看下面這些註釋:

1
2
3
4
5
6
7
8
9
10
11
12
//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."

  避免過多的參數

  經過聲明一個類來代替多個參數。建立一個類,用於包含全部的參數。一般來說,這是一個較好的設計,而且這個抽象很是的有價值。

1
2
3
4
5
6
7
8
9
10
//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)
     {
     }

  咱們須要引入類來代替全部的參數。

  避免複雜的表達式

1
2
3
4
5
if (product.Price>500 && !product.IsDeleted &&
   !product.IsFeatured && product.IsExported)
{
       // do something
}

  複雜的表達式意味着其背後隱藏了一些涵義,咱們能夠經過使用屬性來封裝這些表達式,進而使代碼更易讀些。

  把警告等同於錯誤

  若是你注意看代碼,你會發現一個變量被聲明瞭但從沒被使用過。正常來說,咱們編譯工程後會獲得一個警告,但仍能夠運行工程而不會發生任何錯誤。可是咱們應該儘量地移除這些警告。經過以下步驟能夠在工程上設置將警告等同於錯誤:

  精簡多處返回

  在每段程序中都減小函數返回的數量。假設從底部開始閱讀代碼,你很難意識到有可能在上面的某處已經返回了,這樣的代碼將是很是難理解的。

  僅使用一處返回能夠加強可讀性。若是程序這麼寫的話可能看起來比較乾淨,但不當即返回也意味着須要編寫更多代碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//avoid
       if (product.Price>15)
       {
          return false ;
       }
       else if (product.IsDeleted)
       {
          return false ;
       }
       else if (!product.IsFeatured)
       {
          return false ;
       }
       else if ()
       {
          //.....
       }
       return true ;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//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 個退出點,這會使你很是難理解到底程序內部作了什麼,到底會執行什麼,何時執行。

  關於這一點我獲得了不少人的回覆,一些人贊成這個觀點,有些則不一樣意這是一個好的編碼標準。爲了找出潛在的問題,我作了些單元測試,發現若是複雜的方法包含多個退出點,一般狀況下會須要一組測試來覆蓋全部的路徑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if ( BADFunction() == true )
       {
           // expression
           if ( anotherFunction() == true )
           {
            // expression
            return true ;
           }
           else
           {
                //error
           }
       }
       else
       {
           //error
       }
       return false ;
1
2
3
4
5
6
7
8
9
10
11
12
13
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.

相關文章
相關標籤/搜索