每一個C#開發者必須知道的13件事情
1. 開發流程
程式的 Bug 與瑕疵往往出現於開發流程當中。只要對工具善加利用,就有助於在你釋出程式之前便將問題發現,或避開這些問題。
標準化程式碼書寫
標準化程式碼書寫可以使程式碼更加易於維護,尤其是在程式碼由多個開發者或團隊進行開發與維護時,這一優點更加突出。常見的強制程式碼規範化的工具有:FxCop、StyleCop 和 ReSharper。
開發者語:在掩蓋錯誤之前請仔細地思考這些錯誤,並且去分析結果。不要指望依靠這些工具來在程式碼中尋找錯誤,因為結果可能和你的與其相去甚遠。
程式碼審查
審查程式碼與搭檔程式設計都是很常見的練習,比如開發者刻意去審查他人書寫的程式碼。而其他人很希望發現程式碼開發者的一些 bug,例如編碼錯誤或者執行錯誤。
審查程式碼是一種很有價值的練習,由於很依賴於人工操作,因此很難被量化,準確度也不夠令人滿意。
靜態分析
靜態分析不需要你去執行程式碼,你不必編寫測試案例就可以找出一些程式碼不規範的地方,或者是一些瑕疵的存在。這是一種非常有效地尋找問題的方式,但是你需要有一個不會有太多誤報問題的工具。C#常用的靜態分析工具有 Coverity,CAT,NET,Visual Studio Code Analysis。
動態分析
在你執行程式碼的時候,動態分析工具可以幫你找出這些錯誤:安全漏洞,效能與併發性問題。這種方法是在執行時期的環境下進行分析,正因如此,其有效性便受制於程式碼複雜度。Visual Studio 提供了包括 Concurrency Visualizer, IntelliTrace, and Profiling Tools 在內的大量動態分析工具。
管理者/團隊領導語:開發實踐是練習規避常見陷阱的最好方法。同時也要注意測試工具是否符合你的需求。儘量讓你團隊的程式碼診斷水平處於可控的範圍內。
測試
測試的方式多種多樣:單元測試,系統整合測試,效能測試,滲透測試等等。在開發階段,絕大多數的測試案例是由開發者或測試人員來完成編寫,使程式可以滿足需求。
測試只在執行正確的程式碼時才會有效。在進行功能測試的時候,它還可以用來挑戰開發者的研發與維護速度。
開發最佳實踐
工具的選擇上多花點時間,用正確的工具去解決你關心的問題,不要為開發者增添額外的工作。讓分析工具與測試自動流暢地執行起來去尋找問題,但是要保證程式碼的思想仍然清晰地留在開發者的頭腦當中。
儘可能快地定位診斷出來的問題所在位置(不論是通過靜態分析還是測試得到的錯誤,比如編譯警告,標準違例,問題檢測等)。如果剛出來的問題由於“不關心”而去忽略它,導致該問題後來很難找到,那麼就會給程式碼審閱工作者增加很大的工作量,並且還要祈禱他們不會因此煩躁。
請接受這些有用的建議,讓自己程式碼的質量,安全性,可維護性得到提升,同時也提升開發者們的研發能力、協調能力,以及提升釋出程式碼的可預測性。
目標 | 工具 | 影響 |
一致性,可維護性 | 標準化程式碼書寫,靜態分析,程式碼審查 | 間距一致,命名標準,良好的可讀格式,都會讓開發者更易編寫與維護程式碼。 |
準確性 | 程式碼審查,靜態分析,動態分析,測試 | 程式碼不只是需要語法正確,還需要以開發者的思想來滿足軟體需求。 |
功能性 | 測試 | 測試可以驗證大多數的需求是否得到滿足:正確性,可拓展性,魯棒性以及安全性。 |
安全性 | 標準化程式碼書寫,程式碼審查,靜態分析,動態分析,測試 | 安全性是一個複雜的問題,任何一個小的漏洞都是潛在的威脅。 |
開發者研發能力 | 標準化程式碼書寫,靜態分析,測試 | 開發者在工具的幫助下會很快速地更正錯誤。 |
釋出可預測性 | 標準化程式碼書寫,程式碼審查,靜態分析,動態分析,測試 | 流線型後期階段的活動、最小化錯誤定位迴圈,都可以讓問題發現的更早。 |
2. 型別的陷阱
C#的一個主要的優點就是其靈活的型別系統,而安全的型別可以幫助我們更早地找到錯誤。通過強制執行嚴格的型別規則,編譯器能夠幫助你維持良好的程式碼書寫習慣。在這一方面,C#語言與 .NET 框架為我們提供了大量的型別,以適應絕大多數的需求。雖然許多開發者對一般的型別有著良好的理解,並且也知曉使用者的需求,但是一些誤解與誤用仍然存在。
更多關於 .NTE 框架類庫的資訊請參閱 MSDN library。
理解並使用標準介面
特定的介面涉及到常用的 C# 特徵。例如,IDiposable 允許使用常見的資源管理語言,例如關鍵詞“using”。良好地理解介面可以幫助你書寫通順的 C# 程式碼,並且更易於維護。
避免使用 ICloneable 介面——開發者從來沒搞清楚一個被複制的物件到底是深拷貝還是淺拷貝。由於仍沒有一種對複製物件操作是否正確的標準評判,於是也就沒辦法有意義地去將介面作為一個 contract 去使用。
結構體
儘量避免向結構體中進行寫入,將它們視為一種不變的物件以防止混亂。在像多執行緒這種場景下進行記憶體共享,會變得更安全。我們對結構體採用的方法是,在建立結構體時對其進行初始化操作,如果需要改變其資料,那麼建議生成一個新的實體。
正確理解哪些標準型別/方法是不可變,並且可返回新的值(例如串,日期),用這些來替代那些易變物件(如 List.Enumerator)。
字串
字串的值可能為空,所以可以在合適的時候使用一些比較方便的功能。值判斷(s.Length==0)時可能會出現 NullReferenceException 錯誤,而 String.IsNullOrEmpty (s)和 String.IsNullOrWhitespace (s)可以很好地使用 null。
標記列舉
列舉型別與常量可以使程式碼更加易於閱讀,通過利用識別符號替換幻數,可以表現出值的意義。
如果你需要生成大量的列舉型別,那麼帶有標記的列舉型別是一種更加簡單的選擇:
[Flag]public enum Tag { None =0x0, Tip =0x1, Example=0x2}
下面這種方法可以讓你在一個 snippet 中使用多重標記:
snippet.Tag = Tag.Tip | Tag.Example
這種方法有利於資料的封裝,因此你也不必擔心在使用 Tag property getter 時有內部集合資訊洩露。
Equality comparisons(相等性比較)
有如下兩種型別的相等性:
1. 引用相等性,即兩種引用都指向同一個物件。
2. 數值相等性,即兩個不同的引用物件可以視為相等的。
除此之外,C#還提供了很多相等性的測試方法。最常見的方法如下:
==與!=操作
由物件的虛繼承等值法
靜態 Object.Equal 法
IEquatable<T>介面等值法
靜態 Object.ReferenceEquals 法
有時候很難弄清楚使用引用或值相等性的目的。想進一步弄明白這些,並且讓你的工作做得更好,請參閱:
MSDNhttp://msdn.microsoft.com/en-us/library/dd183752.aspx
如果你想要覆蓋某個東西的時候,不要忘了 MSDN 上為我們提供的諸如 IEquatable<T>, GetHashCode ()之類的工具。
注意無型別容器在過載方面的影響,可以考慮使用“myArrayList[0] == myString”這一方法。陣列元素是編譯階段型別的“物件”,因此引用相等性可以使用。雖然 C# 會向你提醒這些潛在的錯誤,但是在編譯過程中,unexpected reference equality 在某些情況下不會被提醒。
3. 類的陷阱
封裝你的資料
類在恰當管理資料方面起很大的作用。鑑於效能上的一些原因,類總是快取部分結果,或者是在內部資料的一致性上做出一些假設。使資料許可權公開的話會在一定程度上讓你去快取,或者是作出假設,而這些操作是通過對效能、安全性、併發性的潛在影響表現出來的。例如暴露像泛型集合、陣列之類的易變成員項,可以讓使用者跳過你而直接進行結構體的修改。
屬性
除了可以通過 access modifiers 控制物件之外,屬性還可以讓你很精確地掌控使用者與你的物件之間進行了什麼互動。特別要指出的是,屬性還可以讓你瞭解到讀寫的具體情況。
屬效能在通過儲存邏輯將資料覆寫進 getters 與 setters 的時候幫助你建立一個穩定的 API,或是提供一個數據的繫結資源。
永遠不要讓屬性 getter 出現異常,並且也要避免修改物件狀態。這是一種對方法的需求,而不是屬性的 getter。
更多有關屬性的資訊,請參閱 MSDN:
同時也要注意 getter 的一些副作用。開發者也習慣於將成員體的存取視為一種常見的操作,因此他們在程式碼審查的時候也常常忽視那些副作用。
物件初始化
你可以為一個新建立的物件根據它建立的表達形式賦予屬性。例如為 Foo 與 Bar 屬性建立一個新的具有給定值的C類物件:
new C {Foo=blah, Bar=blam}
你也可以生成一個具有特定屬性名稱的匿名型別的實體:
var myAwesomeObject = new {Name=”Foo”, Size=10};
初始化過程在建構函式體之前執行,因此需要保證在輸入至建構函式之前,將這一域給初始化。由於建構函式還沒有執行,所以目標域的初始化可能不管怎樣都不涉及“this”。
過渡規範細化的輸入引數
為了使一些特殊方法更加容易控制,最好在你使用的方法當中使用最少的特定型別。比如在一種方法中使用 List<Bar>進行迭代:
public void Foo (List<Bar> bars) { foreach(var b in bars) { // do something with the bar... } }
對於其他 IEnumerable<Bar>集來說,使用這種方法的表現更加出色一些,但是對於特定的引數 List<Bar>來說,我們更需要使集以表的形式表現。儘量少地選取特定的型別(諸如 IEnumerable<T>, ICollection<T>此類)以保證你的方法效率的最大化。
4. 泛型
泛型是一種在定義獨立型別結構體與設計演算法上一種十分有力的工具,它可以強制型別變得安全。
用像 List<T>這樣的泛型集來替代陣列列表這種無型別集,既可以提升安全性,又可以提升效能。
在使用泛型時,我們可以用關鍵詞“default”來為型別獲取預設值(這些預設值不可以硬編碼寫進 implementation)。特別要指出的是,數字型別的預設值是o,引用型別與空型別的預設值為 null。
T t = default(T);
5. 型別轉換
型別轉換有兩種模式。其一顯式轉換必須由開發者呼叫,另一隱式轉換是基於環境下應用於編譯器的。
常量o可由隱式轉換至列舉型資料。當你嘗試呼叫含有數字的方法時,可以將這些資料轉換成列舉型別。
型別轉換 | 描述 |
Tree tree = (Tree) obj; | 這種方法可以在物件是樹型別時使用;如果物件不是樹,可能會出現 InvalidCast 異常。 |
Tree tree = obj as Tree; | 這種方法你可以在預測物件是否為樹時使用。如果物件不是樹,那麼會給樹賦值 null。你可以用“as”的轉換,然後找到 null 值的返回處,再進行處理。由於它需要有條件處理的返回值,因此記住只在需要的時候才去用這種轉換。這種額外的程式碼可能會造成一些 bug,還可能會降低程式碼的可讀性。 |
轉換通常意味著以下兩件事之一:
1. RuntimeType 的表現可比編譯器所表現出來的特殊的多,Cast 轉換命令編譯器將這種表達視為一種更特殊的型別。如果你的設想不正確的話,那麼編譯器會向你輸出一個異常。例如:將物件轉換成串。
2. 有一種完全不同的型別的值,與 Expression 的值有關。Cast 命令編譯器生成程式碼去與該值相關聯,或者是在沒有值的情況下報出一個異常。例如:將 double 型別轉換成 int 型別。
以上兩種型別的 Cast 都有著風險。第一種 Cast 向我們提出了一個問題:“為什麼開發者能很清楚地知道問題,而編譯器為什麼不能?”如果你處於這個情況當中,你可以去嘗試改變程式讓編譯器能夠順利地推理出正確的型別。如果你認為一個物件的 runtime type 是比 compile time type 還要特殊的型別,你就可以用“as”或者“is”操作。
第二種 cast 也提出了一個問題:“為什麼不在第一步就對目標資料型別進行操作?”如果你需要 int 型別的結果,那麼用 int 會比 double 更有意義一些。
獲取額外的資訊請參閱:
在某些情況下顯式轉換是一種正確的選擇,它可以提高程式碼可閱讀性與 debug 能力,還可以在採用合適的操作的情況下提高測試能力。
6. 異常
異常並不是 condition
異常不應該常出現在程式流程中。它們代表著開發者所不願看到的執行環境,而這些很可能無法修復。如果你期望得到一個可控制的環境,那麼主動去檢查環境會比等待問題的出現要好得多。
利用 TryParse ()方法可以很方便地將格式化的串轉換成數字。不論是否解析成功,它都會返回一個布林型結果,這要比單純返回異常要好很多。
注意使用 exception handling scope
寫程式碼時注意 catch 與 finally 塊的使用。由於這些不希望得到的異常,控制可能進入這些塊中。那些你期望的已執行的程式碼可能會由於異常而跳過。如:
Frobber originalFrobber = null;try { originalFrobber = this.GetCurrentFrobber (); this.UseTemporaryFrobber (); this.frobSomeBlobs (); }finally { this.ResetFrobber (originalFrobber); }
如果 GetCurrentFrobber ()報出了一個異常,那麼當 finally blocks 被執行時 originalFrobber 的值仍然為空。如果 GetCurrentFrobber 不能被扔掉,那麼為什麼其內部是一個 try block?
明智地處理異常
要注意有針對性地處理你的目標異常,並且只去處理目的碼當中的異常部分。儘量不要去處理所有異常,或者是根類異常,除非你的目的是記錄並重新處理這些異常。某些異常會使應用處於一種接近崩潰的狀態,但這也比無法修復要好得多。有些試圖修復程式碼的操作可能會誤使情況變得更糟糕。
關於致命的異常都有一些細微的差異,特別是注重 finally blocks 的執行,可以影響到異常的安全與除錯。更多資訊請參閱:
使用一款頂級的異常處理器去安全地處理異常情況,並且會將 debug 的一些問題資訊暴露出來。使用 catch 塊會比較安全地定位那些特殊的情況,從而安全地解決這些問題,再將一些問題留給頂級的異常處理器去解決。
如果你發現了一個異常,請做些什麼去解決它,而不要去將這個問題擱置。擱置只會使問題更加複雜,更難以解決。
將異常包含至一個自定義異常中,對面向公共 API 的