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#還提供了很多相等性的測試方法。最常見的方法如下:
有時候很難弄清楚使用引用或值相等性的目的。想進一步弄明白這些,並且讓你的工作做得更好,請參閱:
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:
http://msdn.microsoft.com/en-us/library/ms229006(v=vs.120).aspx
同時也要注意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更有意義一些。
擷取額外的資訊請參閱:
http://blogs.msdn.com/b/ericlippert/archive/tags/cast+operator/
在某些情況下顯式轉換是一種正確的選擇,它可以提高代碼可閱讀性與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的執行,可以影響到異常的安全與調試。更多資訊請參閱:
http://incrediblejourneysintotheknown.blogspot.com/2009/02/fatal-exceptions-and-why-vbnet-has.html
使用一款頂級的異常處理器去安全地處理異常情況,並且會將debug的一些問題資訊暴露出來。使用catch塊會比較安全地定位那些特殊的情況,從而安全地解決這些問題,再將一些問題留給頂級的異常處理器去解決。
如果你發現了一個異常,請做些什麼去解決它,而不要去將這個問題擱置。擱置只會使問題更加複雜,更難以解決。
將異常包含至一個自訂異常中,對面向公用API的代碼特別有用。異常是視覺化介面方法的一部分,它也被參數與傳回值所控制。但這種擴散了很多異常的方法對於代碼的魯棒性與可維護性的解決來說十分麻煩。
拋出(Throw)與繼續拋出(ReThrow)異常
如果你希望在更高層次上解決caught異常,那麼就維持原異常狀態,並且棧就是一個很好的debug方法。但需要注意維持好debug與安全考慮的平衡。
好的選擇包括簡單地將異常繼續拋出:
Throw;
或者將異常視為內部異常重新拋出:
拋出一個新CustomException;
不要顯式重新拋出類似於這樣的caught異常:
Throw e;
這樣的話會將異常的處理恢複至初始狀態,並且阻礙debug。
有些異常發生於你代碼的運行環境之外。與其使用caught塊,你可能更需要向目標當中添加如ThreadException或UnhandledException之類的處理器。例如,Windows表單異常並不是出現於表單處理線程環境當中的。
原子性(資料完整性)
千萬不要讓異常影響到你資料模型的完整性。你需要保證你的對象處於比較穩定的狀態當中——這樣一來任何由類的執行的操作都不會出現違例。否則,通過“恢複”這一手段會使你的代碼變得更加讓人不解,也容易造成進一步的損壞。
考慮幾種修改私人域順序的方法。如果在修改順序的過程當中出現了異常,那麼你的對象可能並不處於非法狀態下。嘗試在實際更新網域之前去得到新的值,這樣你就可以在異常安全管理下,正常地更新你的域。
對特定類型的值——包括布爾型,32bit或者更小的資料類型與引用型——進行可變數的分配,確保可以是原子型。沒有什麼保障是給一些大型資料(double,long,decimal)使用的。可以多考慮這個:在共用多線程的變數時,多使用lock statements。
7.事件
事件與委託共同提供了一種關於類的方法,這種方法在有特殊的事情發生時向使用者進行提醒。委託事件的值在事件發生時應被調用。事件就像是委託類型的域,當對象產生時,其自動初始化為null。
事件也像值為“組播”的域。這也就是說,一種委託可以依次調用其他委託。你可以將一個委託分配給一個事件,你也可以通過類似-=於+=這樣的操作來控制事件。
注意資源競爭
如果一個事件被多個線程所共用,另一個線程就有可能在你檢查是否為null之後,在調用其之前而清除所有的使用者資訊——並拋出一個NullReferenceException。
對於此類問題的標準解決方案是建立一個該事件的副本,用於測試與調用。你仍然需要注意的是,如果委託沒有被正確調用的話,那麼在其他線程裡被移除的使用者仍然可以繼續操作。你也可以用某種方法將操作按順序鎖定,以避免一些問題。
public event EventHandler SomethingHappened;private void OnSomethingHappened(){ // The event is null until somebody hooks up to it // Create our own copy of the event to protect against another thread removing our subscribers EventHandler handler = SomethingHappened; if (handler != null) handler(this,new EventArgs());}
更多關於事件與競爭的資訊請參閱:
http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx
不要忘記將事件處理器Unhook
使用一種事件處理器為事件資源產生一個由處理器的資來源物件到接收對象的引用,可以保護接收端的garbage collection。
適當的unhook處理器可以確保你不必因委託不再工作而去調用它浪費時間,也不會使記憶體儲存無用委託與不可引用的對象。
8.屬性
屬性提供了一種向程式集、類與其資訊屬性中注入中繼資料的方法。它們經常用來提供資訊給代碼的消費者——比如debugger、架構測試、應用——通過反射這一方式。你也可以向你的使用者定義屬性,或是使用預定義屬性,詳見下表:
屬性 |
使用對象 |
目的 |
DebuggerDisplay |
Debugger |
Debugger display 格式 |
InternalsVisibleTo |
Member access |
使用特定類來暴露內部成員去指定其他的類。基於此方法,測試方法可以用來保護成員,並且persistence層可以用一些特殊的隱蔽方法。 |
DefaultValue |
Properties |
為屬性指定一個預設值 |
一定要對DebuggerStepThrough多重視幾分——否則它會在這個方法應用的地方讓尋找bug變得十分困難,你也會因此而跳過某步或是推倒而重做它。
9.Debug
Debug是在開發過程中必不可少的部分。除了使運行環境不透明的部分變得可視化之外,debugger也可以侵入運行環境,並且如果不使用debugger的話會導致應用程式變現有所不同。
使異常棧可視化
為了觀察當前架構異常狀態,你可以將“$exception”這一表達添加進Visual Studio Watch視窗。這種變數包含了當前異常狀態,類似於你在catch block中所看見的,但其中不包含在debugger中看見的不是代碼中的真正存在的異常。
注意訪問器的副作用
如果你的屬性有副作用,那麼考慮你是否應使用特性或者是debugger設定去避免debugger自動地調用getter。例如,你的類可能有這樣一個屬性:
private int remainingAccesses = 10;private string meteredData;public string MeteredData{ get { if (remainingAccesses-- > 0) return meteredData; return null; }}
你第一次在debugger中看見這個對象時,remainingAccesses會獲得一個值為10的整型變數,並且MeteredData為null。然而如果你hover結束了remainingAccesses,你會發現它的值會變成9.這樣一來debugger的屬性值表現改變了你的對象的狀態。
10.效能最佳化
早做計劃,不斷監測,後做最佳化
在設計階段,制定切實可行的目標。在開發階段,專註於代碼的正確性要比去做微調整有意義的多。對於你的目標,你要在開發過程中多進行監測。只需要在你沒有達到預期的目標的時候,你才應該去花時間對程式做一個調整。
請記住用合適的工具來確保效能的經驗性測量,並且使測試處於這樣一種環境當中:可反覆多次測試,並且測試過程盡量與現實當中使用者的使用習慣一致。
當你對效能進行測試的時候,一定要注意你真正所關心的測試目標是什麼。在進行某一項功能的測試時,你的測試有沒有包含這項功能的調用或者是迴路構造的開銷?
我們都聽說過很多比別人做得快很多的項目神話,不要盲目相信這些,實驗與測試才是實在的東西。
由於CLR最佳化的原因,有時候看起來效率不高的代碼可能會比看起來效率高的代碼啟動並執行更快。例如,CLR最佳化迴圈覆蓋了一個完整的數組,以避免在不可見的per-element範圍裡的檢查。開發人員經常在迴圈一個數組之前先計算一下它的長度:
int[] a_val = int[4000];int len = a_val.Length;for (int i = 0; i < len; i++) a_val[i] = i;
通過將長度儲存進一個變數當中,CLR會不去識別這一部分,並且跳過最佳化。但是有時手動最佳化會反人類地導致更糟糕的效能表現。
構造字串
如果你打算將大量的字串進行串連,可以使用System.Text.StringBuilder來避免產生大量的臨時字串。
對集合使用批量處理
如果你打算產生並填滿集合中已知的大量資料,由於再分配的存在,可以用保留空間來解決產生集合的效能與資源問題。你可以用AddRange方法來進一步對效能進行最佳化,如下在List<T>中處理:
Persons.AddRange(listBox.Items);
11.資源管理
垃圾收集器(garbage collector)可以自動地清理記憶體。即使這樣,一切被拋棄的資源也需要適當的處理——特別是那些垃圾收集器不能管理的資源。
資源管理問題的常見來源 |
|
記憶體片段 |
如果沒有足夠大的連續的虛擬位址儲存空間,可能會導致分配失敗 |
進程限制 |
進程通常都可以讀取記憶體的所有子集,以及系統可用的資源。 |
資源流失 |
垃圾收集器只管理記憶體,其他資源需要由應用程式正確管理。 |
不穩定資源 |
那些依賴於垃圾收集器與終結器(finalizers)的資源在很久沒用過的時候,不可被立即調用。實際上它們可能永遠不可能被調用。 |
|
|
利用try/finally block來確保資源已被合理釋放,或是讓你的類使用IDisposable,以及更方便更安全的聲明方式。
using (StreamReader reader=new StreamReader(file)) { //your code here
在產品代碼中避免garbage collector
除了用調用GC.Collect()幹擾garbage collector之外,也可以考慮適當地釋放或是拋棄資源。在進行效能測試時,如果你可以承擔這種影響帶來的後果,你再去使用garbage collector。
避免編寫finalizers
與當前一些流傳的謠言不同的是,你的類不需要Finalizers,而這隻是因為IDisposable的存在!你可以讓IDisposable賦予你的類在任何已擁有的組合執行個體中調用Dispose的能力,但是finalizers只能在擁有未管理的資源類中使用。
Finalizers主要對互動式Win32位控制代碼API有很大作用,並且SafeHandle控制代碼是很容易利用的。
不要總是設想你的finalizers(總是在finalizer線程上啟動並執行)會很好地與其他對象進行互動。那些其他的對象可能在該進程之前就被終止掉了。
12.並發性
處理並發性與多線程編程是件複雜的、困難的事情。在將並發性添加進你的程式之前,請確保你已經明確瞭解你的做的是什麼——因為這裡面有太多門道了!
多線程軟體的情況很難進行預測,比如很容易產生如競爭條件與死結的問題,而這些問題並不是僅僅影響單線程應用。基於這些風險,你應該將多線程視為最後一種手段。如果不得不使用多線程,盡量縮減多線程同時使用記憶體的需求。如果必須使線程同步,請儘可能地使用最高等級的同步機制。在最高等級的前提下,包括了這些機制:
以上的這些很難解釋清楚C#/.NET的複雜之處。如果你想開發一個正常的並發應用,可以去參閱O’Reilly的《Concurrency in C# Cookboo》。
使用Volatile
將一個域標記為“volatile”是一種進階特性,而這種設定也經常被專家所誤解。C#的編譯器會保證目標域可以被擷取與釋放語義,但是被lock的域就不適用於這種情況。如果你不知道擷取什麼,不知道釋放什麼語義,以及它們是怎樣影響CPU層次的最佳化,那麼久避免使用volatile域。取而代之的可以用更高層次的工具,比如Task Parallel Library或是CancellationToken。
安全執行緒與內建方法
標準庫類型常提供使對象安全執行緒更容易的方法。例如Dictionary.TryGetValue()。使用此類方法一般可以使你的代碼變得更加清爽,並且你也不必擔心像TOCTOU(time-of-check-time-of-use競爭危害的一種)這樣的資料競爭。
不要鎖住“this”、字串,或是其他普通public的對象
當使用在多線程環境下的一些類時,多注意lock的使用。鎖住字串常量,或是其他公用對象,會阻止你鎖狀態下的封裝,還可能會導致死結。你需要阻止其他代碼鎖定在同一使用的對象上,當然你最好的選擇是使用private對象成員項。
13.避免常見的錯誤
Null
濫用null是一種常見的導致程式錯誤的來源,這種非正常操作可能會使程式崩潰或是其他的異常。如果你試圖擷取一個null的引用,就好像它是某對象的有效引用值(例如通過擷取一個屬性或是方法),那麼在運行時就會拋出一個NullReferenceException。
靜態與動態分析工具可以在你發布代碼之前為你檢查出潛在的NullReferenceException。在C#當中,引用型為null通常是由於變數沒有引用到某個對象而造成的。對於值可為空白的類型與引用型來說,是可以使用null的。例如:Nullable<Int>,空委託,已登出的事件,“as”轉化失敗的,以及一些其他的情況。
每個null引用異常都是一個bug。相比於找到NullReferenceException這個問題來說,不如嘗試在你使用該對象之前去為null進行測試。這樣一來可以使代碼更易於最小化的try/catch block讀取。
當從資料庫表中讀取資料時,注意缺失值可以表示為DBNull 對象,而不是作為空白引用。不要期望它們表現得像潛在的Null 參考一樣。
用二進位的數字表示十進位的值
Float與double都可以表示十進位實數,但不能表示二進位實數,並且在儲存十進位值的時候可以在必要時用二進位的近似值儲存。從十進位的角度來看,這些二進位的近似值通常都有不同的精度與取捨,有時在算數操作當中會導致一些不期望的結果。由於浮點型運算通常在硬體當中執行,因此硬體條件的不可預測會使這些差異更加複雜。
在十進位精度很重要的時候,就要使用十進位了——比如經濟方面的計算。
調整結構
有一種常見的錯誤就是忘記了結構是實值型別,意即其複製與通過值傳遞。例如你可能見過這樣的代碼:
struct P { public int x; public int y; }void M(){ P p = whatever; … p.x = something; … N(p);
忽然某一天,代碼維護人員決定將代碼重構成這樣:
void M(){ P p = whatever; Helper(p); N(p);}void Helper(P p){ … p.x = something;
現在當N(p)在M()中被調用,p就有了一個錯誤的值。調用Helper(p)傳遞p的副本,並不是引用p,於是在Helper()中的突變便丟失掉了。如果被正常調用,那麼Helper應該傳遞的是調整過的p的副本。
非預期計算
C#編譯器可以保護在運算過程中的常量溢出,但不一定是計算值。使用“checked”與“unchecked”兩個關鍵詞來標記你想對變數進行什麼操作。
不儲存傳回值
與結構體不同的是,類是參考型別,並且可以適當地修改引用對象。然而並不是所有的對象方法都可以實際修改引用對象,有一些返回的是一個新的對象。當開發人員調用後者時,他們需要記住將傳回值分配給一個變數,這樣才可以使用修改過的對象。在代碼審查階段,這些問題的類型通常會逃過審查而不被發現。像字串之類的對象,它們是不可變的,因此永遠不可能修改這些對象。即便如此,開發人員還是很容易忘記這些問題。
例如,看如下 string.Replace()代碼:
string label = “My name is Aloysius”;label.Replace(“Aloysius”, “secret”);
這兩行代碼運行之後會列印出“My name is Aloysius” ,這是因為Raeplace方法並沒改變該字串的值。
不要使迭代器與列舉程式失效
注意不要在遍曆時去修改集合
List<Int> myItems = new List<Int>{20,25,9,14,50};foreach(int item in myItems){ if (item < 10) { myItems.Remove(item); // iterator is now invalid! // you’ll get an exception on the next iteration
如果你運行了這個代碼,那麼它一在下一項的集合中進行迴圈,你就會得到一個異常。
正確的處理方法是使用第二個list去儲存你想刪除的這一項,然後在你想刪除的時候再遍曆這個list:
List<Int> myItems = new List<Int>{20,25,9,14,50};List<Int> toRemove = new List<Int>();foreach(int item in myItems){ if (item < 10) { toRemove.Add(item); }}foreach(int item in toRemove){
如果你用的是C#3.0或更高版本,可以嘗試List<T>.RemoveAll:
myInts.RemoveAll(item => (item < 10));
屬性名稱錯誤
在實現屬性時,要注意屬性的名稱和在類當中用的成員項的名字有很大差別。很容易在不知情的情況下使用了相同的名稱,並且在屬性被擷取的時候還會觸發死迴圈。
// The following code will trigger infinite recursionprivate string name;public string Name{ get { return Name; // should reference “name” instead.
在重新命名間接屬性時同樣要小心。例如:在WPF中繫結資料將屬性名稱指定為字串。有時無意的改變屬性名稱,可能會不小心造成編譯器無法解決的問題。
英文原文:13 Things Every C# Developer Should Know 翻譯:碼農網