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

2016-07-16    分類:.NET開發、推薦閱讀、程式設計開發、首頁精華2人評論發表於2016-07-16

本文由碼農網 – 唐昊陽原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

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中繫結的資料將屬性名稱指定為字串。有時無意的改變屬性名稱,可能會不小心造成編譯器無法解決的問題。

譯文連結:http://www.codeceo.com/article/13-things-every-csharp-developer-should-know.html
英文原文:13 Things Every C# Developer Should Know
翻譯作者:碼農網 – 唐昊陽
轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]

相關文章