1. 程式人生 > >C#開發者必須知道的13件事情

C#開發者必須知道的13件事情

解釋 無法 返回 以及 規範 IT 可控 另一個 線程安全

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:

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.並發性

處理並發性與多線程編程是件復雜的、困難的事情。在將並發性添加進你的程序之前,請確保你已經明確了解你的做的是什麽——因為這裏面有太多門道了!

多線程軟件的情況很難進行預測,比如很容易產生如競爭條件與死鎖的問題,而這些問題並不是僅僅影響單線程應用。基於這些風險,你應該將多線程視為最後一種手段。如果不得不使用多線程,盡量縮減多線程同時使用內存的需求。如果必須使線程同步,請盡可能地使用最高等級的同步機制。在最高等級的前提下,包括了這些機制:

  • Async-await/Task Parallel Library/Lazy<T>
  • Lock/monitor/AutoResetEvent
  • Interlocked/Semaphore
  • 可變域與顯式barrier

以上的這些很難解釋清楚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 對象,而不是作為空引用。不要期望它們表現得像潛在的空引用一樣。

用二進制的數字表示十進制的值

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 recursion
private string name;
public string Name
{
    get
    {
        return Name;  // should reference “name” instead.

在重命名間接屬性時同樣要小心。例如:在WPF中綁定的數據將屬性名稱指定為字符串。有時無意的改變屬性名稱,可能會不小心造成編譯器無法解決的問題。

轉自:https://www.cnblogs.com/hgmyz/p/5811360.html

C#開發者必須知道的13件事情