溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

C#開發(fā)者需要注意什么

發(fā)布時間:2021-09-09 16:47:54 來源:億速云 閱讀:115 作者:小新 欄目:編程語言

小編給大家分享一下C#開發(fā)者需要注意什么,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

1.開發(fā)流程

程序的Bug與瑕疵往往出現(xiàn)于開發(fā)流程當中。只要對工具善加利用,就有助于在你發(fā)布程序之前便將問題發(fā)現(xiàn),或避開這些問題。

標準化代碼書寫

標準化代碼書寫可以使代碼更加易于維護,尤其是在代碼由多個開發(fā)者或團隊進行開發(fā)與維護時,這一優(yōu)點更加突出。常見的強制代碼規(guī)范化的工具有:FxCop、StyleCop和ReSharper。

開發(fā)者語:在掩蓋錯誤之前請仔細地思考這些錯誤,并且去分析結(jié)果。不要指望依靠這些工具來在代碼中尋找錯誤,因為結(jié)果可能和你的與其相去甚遠。

代碼審查

審查代碼與搭檔編程都是很常見的練習,比如開發(fā)者刻意去審查他人書寫的代碼。而其他人很希望發(fā)現(xiàn)代碼開發(fā)者的一些bug,例如編碼錯誤或者執(zhí)行錯誤。

審查代碼是一種很有價值的練習,由于很依賴于人工操作,因此很難被量化,準確度也不夠令人滿意。

靜態(tài)分析

靜態(tài)分析不需要你去運行代碼,你不必編寫測試案例就可以找出一些代碼不規(guī)范的地方,或者是一些瑕疵的存在。這是一種非常有效地尋找問題的方式,但是你需要有一個不會有太多誤報問題的工具。C#常用的靜態(tài)分析工具有Coverity,CAT,NET,Visual Studio Code Analysis。

動態(tài)分析

在你運行代碼的時候,動態(tài)分析工具可以幫你找出這些錯誤:安全漏洞,性能與并發(fā)性問題。這種方法是在執(zhí)行時期的環(huán)境下進行分析,正因如此,其有效性便受制于代碼復(fù)雜度。Visual Studio提供了包括Concurrency Visualizer, IntelliTrace, and Profiling Tools在內(nèi)的大量動態(tài)分析工具。

管理者/團隊領(lǐng)導(dǎo)語:開發(fā)實踐是練習規(guī)避常見陷阱的最好方法。同時也要注意測試工具是否符合你的需求。盡量讓你團隊的代碼診斷水平處于可控的范圍內(nèi)。

測試

測試的方式多種多樣:單元測試,系統(tǒng)集成測試,性能測試,滲透測試等等。在開發(fā)階段,絕大多數(shù)的測試案例是由開發(fā)者或測試人員來完成編寫,使程序可以滿足需求。

測試只在運行正確的代碼時才會有效。在進行功能測試的時候,它還可以用來挑戰(zhàn)開發(fā)者的研發(fā)與維護速度。

開發(fā)最佳實踐

工具的選擇上多花點時間,用正確的工具去解決你關(guān)心的問題,不要為開發(fā)者增添額外的工作。讓分析工具與測試自動流暢地運行起來去尋找問題,但是要保證代碼的思想仍然清晰地留在開發(fā)者的頭腦當中。

盡可能快地定位診斷出來的問題所在位置(不論是通過靜態(tài)分析還是測試得到的錯誤,比如編譯警告,標準違例,問題檢測等)。如果剛出來的問題由于“不關(guān)心”而去忽略它,導(dǎo)致該問題后來很難找到,那么就會給代碼審閱工作者增加很大的工作量,并且還要祈禱他們不會因此煩躁。

請接受這些有用的建議,讓自己代碼的質(zhì)量,安全性,可維護性得到提升,同時也提升開發(fā)者們的研發(fā)能力、協(xié)調(diào)能力,以及提升發(fā)布代碼的可預(yù)測性。

目標 工具 影響
一致性,可維護性 標準化代碼書寫,靜態(tài)分析,代碼審查 間距一致,命名標準,良好的可讀格式,都會讓開發(fā)者更易編寫與維護代碼。
準確性 代碼審查,靜態(tài)分析,動態(tài)分析,測試 代碼不只是需要語法正確,還需要以開發(fā)者的思想來滿足軟件需求。
功能性 測試 測試可以驗證大多數(shù)的需求是否得到滿足:正確性,可拓展性,魯棒性以及安全性。
安全性 標準化代碼書寫,代碼審查,靜態(tài)分析,動態(tài)分析,測試 安全性是一個復(fù)雜的問題,任何一個小的漏洞都是潛在的威脅。
開發(fā)者研發(fā)能力 標準化代碼書寫,靜態(tài)分析,測試 開發(fā)者在工具的幫助下會很快速地更正錯誤。
發(fā)布可預(yù)測性 標準化代碼書寫,代碼審查,靜態(tài)分析,動態(tài)分析,測試 流線型后期階段的活動、最小化錯誤定位循環(huán),都可以讓問題發(fā)現(xiàn)的更早。

2.類型的陷阱

C#的一個主要的優(yōu)點就是其靈活的類型系統(tǒng),而安全的類型可以幫助我們更早地找到錯誤。通過強制執(zhí)行嚴格的類型規(guī)則,編譯器能夠幫助你維持良好的代碼書寫習慣。在這一方面,C#語言與.NET框架為我們提供了大量的類型,以適應(yīng)絕大多數(shù)的需求。雖然許多開發(fā)者對一般的類型有著良好的理解,并且也知曉用戶的需求,但是一些誤解與誤用仍然存在。

更多關(guān)于.NTE框架類庫的信息請參閱MSDN library。

理解并使用標準接口

特定的接口涉及到常用的C#特征。例如,IDiposable允許使用常見的資源管理語言,例如關(guān)鍵詞“using”。良好地理解接口可以幫助你書寫通順的C#代碼,并且更易于維護。

避免使用ICloneable接口——開發(fā)者從來沒搞清楚一個被復(fù)制的對象到底是深拷貝還是淺拷貝。由于仍沒有一種對復(fù)制對象操作是否正確的標準評判,于是也就沒辦法有意義地去將接口作為一個contract去使用。

結(jié)構(gòu)體

盡量避免向結(jié)構(gòu)體中進行寫入,將它們視為一種不變的對象以防止混亂。在像多線程這種場景下進行內(nèi)存共享,會變得更安全。我們對結(jié)構(gòu)體采用的方法是,在創(chuàng)建結(jié)構(gòu)體時對其進行初始化操作,如果需要改變其數(shù)據(jù),那么建議生成一個新的實體。

正確理解哪些標準類型/方法是不可變,并且可返回新的值(例如串,日期),用這些來替代那些易變對象(如List.Enumerator)。

字符串

字符串的值可能為空,所以可以在合適的時候使用一些比較方便的功能。值判斷(s.Length==0)時可能會出現(xiàn)NullReferenceException錯誤,而String.IsNullOrEmpty(s)和String.IsNullOrWhitespace(s)可以很好地使用null。

標記枚舉

枚舉類型與常量可以使代碼更加易于閱讀,通過利用標識符替換幻數(shù),可以表現(xiàn)出值的意義。

如果你需要生成大量的枚舉類型,那么帶有標記的枚舉類型是一種更加簡單的選擇:

[Flag]
public enum Tag {
  None   =0x0,
  Tip    =0x1,
  Example=0x2
}

下面這種方法可以讓你在一個snippet中使用多重標記:

snippet.Tag = Tag.Tip | Tag.Example

這種方法有利于數(shù)據(jù)的封裝,因此你也不必擔心在使用Tag property getter時有內(nèi)部集合信息泄露。

Equality comparisons(相等性比較)

有如下兩種類型的相等性:

1.引用相等性,即兩種引用都指向同一個對象。

2.數(shù)值相等性,即兩個不同的引用對象可以視為相等的。

除此之外,C#還提供了很多相等性的測試方法。最常見的方法如下:

  • ==與!=操作


  • 由對象的虛繼承等值法


  • 靜態(tài)Object.Equal法


  • IEquatable<T>接口等值法


  • 靜態(tài)Object.ReferenceEquals法

有時候很難弄清楚使用引用或值相等性的目的。想進一步弄明白這些,并且讓你的工作做得更好,請參閱:

MSDNhttp://msdn.microsoft.com/en-us/library/dd183752.aspx

如果你想要覆蓋某個東西的時候,不要忘了MSDN上為我們提供的諸如IEquatable<T>, GetHashCode()之類的工具。

注意無類型容器在重載方面的影響,可以考慮使用“myArrayList[0] == myString”這一方法。數(shù)組元素是編譯階段類型的“對象”,因此引用相等性可以使用。雖然C#會向你提醒這些潛在的錯誤,但是在編譯過程中,unexpected reference equality在某些情況下不會被提醒。

3.類的陷阱

封裝你的數(shù)據(jù)

類在恰當管理數(shù)據(jù)方面起很大的作用。鑒于性能上的一些原因,類總是緩存部分結(jié)果,或者是在內(nèi)部數(shù)據(jù)的一致性上做出一些假設(shè)。使數(shù)據(jù)權(quán)限公開的話會在一定程度上讓你去緩存,或者是作出假設(shè),而這些操作是通過對性能、安全性、并發(fā)性的潛在影響表現(xiàn)出來的。例如暴露像泛型集合、數(shù)組之類的易變成員項,可以讓用戶跳過你而直接進行結(jié)構(gòu)體的修改。

屬性

除了可以通過access modifiers控制對象之外,屬性還可以讓你很精確地掌控用戶與你的對象之間進行了什么交互。特別要指出的是,屬性還可以讓你了解到讀寫的具體情況。

屬性能在通過存儲邏輯將數(shù)據(jù)覆寫進getters與setters的時候幫助你建立一個穩(wěn)定的API,或是提供一個數(shù)據(jù)的綁定資源。

永遠不要讓屬性getter出現(xiàn)異常,并且也要避免修改對象狀態(tài)。這是一種對方法的需求,而不是屬性的getter。

更多有關(guān)屬性的信息,請參閱MSDN:

http://msdn.microsoft.com/en-us/library/ms229006(v=vs.120).aspx

同時也要注意getter的一些副作用。開發(fā)者也習慣于將成員體的存取視為一種常見的操作,因此他們在代碼審查的時候也常常忽視那些副作用。

對象初始化

你可以為一個新創(chuàng)建的對象根據(jù)它創(chuàng)建的表達形式賦予屬性。例如為Foo與Bar屬性創(chuàng)建一個新的具有給定值的C類對象:

new C {Foo=blah, Bar=blam}

你也可以生成一個具有特定屬性名稱的匿名類型的實體:

var myAwesomeObject = new {Name=”Foo”, Size=10};

初始化過程在構(gòu)造函數(shù)體之前運行,因此需要保證在輸入至構(gòu)造函數(shù)之前,將這一域給初始化。由于構(gòu)造函數(shù)還沒有運行,所以目標域的初始化可能不管怎樣都不涉及“this”。

過渡規(guī)范細化的輸入?yún)?shù)

為了使一些特殊方法更加容易控制,最好在你使用的方法當中使用最少的特定類型。比如在一種方法中使用 List<Bar>進行迭代:

public void Foo(List<Bar> bars) 
{
  foreach(var b in bars)
  {
    // do something with the bar...
  }
}

對于其他IEnumerable<Bar>集來說,使用這種方法的表現(xiàn)更加出色一些,但是對于特定的參數(shù)List<Bar>來說,我們更需要使集以表的形式表現(xiàn)。盡量少地選取特定的類型(諸如IEnumerable<T>, ICollection<T>此類)以保證你的方法效率的最大化。

4.泛型

泛型是一種在定義獨立類型結(jié)構(gòu)體與設(shè)計算法上一種十分有力的工具,它可以強制類型變得安全。

用像List<T>這樣的泛型集來替代數(shù)組列表這種無類型集,既可以提升安全性,又可以提升性能。

在使用泛型時,我們可以用關(guān)鍵詞“default”來為類型獲取缺省值(這些缺省值不可以硬編碼寫進implementation)。特別要指出的是,數(shù)字類型的缺省值是o,引用類型與空類型的缺省值為null。

T t = default(T);

5.類型轉(zhuǎn)換

類型轉(zhuǎn)換有兩種模式。其一顯式轉(zhuǎn)換必須由開發(fā)者調(diào)用,另一隱式轉(zhuǎn)換是基于環(huán)境下應(yīng)用于編譯器的。

常量o可由隱式轉(zhuǎn)換至枚舉型數(shù)據(jù)。當你嘗試調(diào)用含有數(shù)字的方法時,可以將這些數(shù)據(jù)轉(zhuǎn)換成枚舉類型。

類型轉(zhuǎn)換 描述
Tree tree = (Tree)obj; 這種方法可以在對象是樹類型時使用;如果對象不是樹,可能會出現(xiàn)InvalidCast異常。
Tree tree = obj as Tree; 這種方法你可以在預(yù)測對象是否為樹時使用。如果對象不是樹,那么會給樹賦值null。你可以用“as”的轉(zhuǎn)換,然后找到null值的返回處,再進行處理。由于它需要有條件處理的返回值,因此記住只在需要的時候才去用這種轉(zhuǎn)換。這種額外的代碼可能會造成一些bug,還可能會降低代碼的可讀性。

轉(zhuǎn)換通常意味著以下兩件事之一:

1.RuntimeType的表現(xiàn)可比編譯器所表現(xiàn)出來的特殊的多,Cast轉(zhuǎn)換命令編譯器將這種表達視為一種更特殊的類型。如果你的設(shè)想不正確的話,那么編譯器會向你輸出一個異常。例如:將對象轉(zhuǎn)換成串。

2.有一種完全不同的類型的值,與Expression的值有關(guān)。Cast命令編譯器生成代碼去與該值相關(guān)聯(lián),或者是在沒有值的情況下報出一個異常。例如:將double類型轉(zhuǎn)換成int類型。

以上兩種類型的Cast都有著風險。第一種Cast向我們提出了一個問題:“為什么開發(fā)者能很清楚地知道問題,而編譯器為什么不能?”如果你處于這個情況當中,你可以去嘗試改變程序讓編譯器能夠順利地推理出正確的類型。如果你認為一個對象的runtime type是比compile time type還要特殊的類型,你就可以用“as”或者“is”操作。

第二種cast也提出了一個問題:“為什么不在第一步就對目標數(shù)據(jù)類型進行操作?”如果你需要int類型的結(jié)果,那么用int會比double更有意義一些。

獲取額外的信息請參閱:

http://blogs.msdn.com/b/ericlippert/archive/tags/cast+operator/

在某些情況下顯式轉(zhuǎn)換是一種正確的選擇,它可以提高代碼可閱讀性與debug能力,還可以在采用合適的操作的情況下提高測試能力。

6.異常

異常并不是condition

異常不應(yīng)該常出現(xiàn)在程序流程中。它們代表著開發(fā)者所不愿看到的運行環(huán)境,而這些很可能無法修復(fù)。如果你期望得到一個可控制的環(huán)境,那么主動去檢查環(huán)境會比等待問題的出現(xiàn)要好得多。

利用TryParse()方法可以很方便地將格式化的串轉(zhuǎn)換成數(shù)字。不論是否解析成功,它都會返回一個布爾型結(jié)果,這要比單純返回異常要好很多。

注意使用exception handling scope

寫代碼時注意catch與finally塊的使用。由于這些不希望得到的異常,控制可能進入這些塊中。那些你期望的已執(zhí)行的代碼可能會由于異常而跳過。如:

Frobber originalFrobber = null;
try {
  originalFrobber = this.GetCurrentFrobber();
  this.UseTemporaryFrobber();
  this.frobSomeBlobs();
}
finally {
  this.ResetFrobber(originalFrobber);
}

如果GetCurrentFrobber()報出了一個異常,那么當finally blocks被執(zhí)行時originalFrobber的值仍然為空。如果GetCurrentFrobber不能被扔掉,那么為什么其內(nèi)部是一個try block?

明智地處理異常

要注意有針對性地處理你的目標異常,并且只去處理目標代碼當中的異常部分。盡量不要去處理所有異常,或者是根類異常,除非你的目的是記錄并重新處理這些異常。某些異常會使應(yīng)用處于一種接近崩潰的狀態(tài),但這也比無法修復(fù)要好得多。有些試圖修復(fù)代碼的操作可能會誤使情況變得更糟糕。

關(guān)于致命的異常都有一些細微的差異,特別是注重finally blocks的執(zhí)行,可以影響到異常的安全與調(diào)試。更多信息請參閱:

http://incrediblejourneysintotheknown.blogspot.com/2009/02/fatal-exceptions-and-why-vbnet-has.html

使用一款頂級的異常處理器去安全地處理異常情況,并且會將debug的一些問題信息暴露出來。使用catch塊會比較安全地定位那些特殊的情況,從而安全地解決這些問題,再將一些問題留給頂級的異常處理器去解決。

如果你發(fā)現(xiàn)了一個異常,請做些什么去解決它,而不要去將這個問題擱置。擱置只會使問題更加復(fù)雜,更難以解決。

將異常包含至一個自定義異常中,對面向公共API的代碼特別有用。異常是可視界面方法的一部分,它也被參數(shù)與返回值所控制。但這種擴散了很多異常的方法對于代碼的魯棒性與可維護性的解決來說十分麻煩。

拋出(Throw)與繼續(xù)拋出(ReThrow)異常

如果你希望在更高層次上解決caught異常,那么就維持原異常狀態(tài),并且棧就是一個很好的debug方法。但需要注意維持好debug與安全考慮的平衡。

好的選擇包括簡單地將異常繼續(xù)拋出:

Throw;

或者將異常視為內(nèi)部異常重新拋出:

拋出一個新CustomException;

不要顯式重新拋出類似于這樣的caught異常:

Throw e;

這樣的話會將異常的處理恢復(fù)至初始狀態(tài),并且阻礙debug。

有些異常發(fā)生于你代碼的運行環(huán)境之外。與其使用caught塊,你可能更需要向目標當中添加如ThreadException或UnhandledException之類的處理器。例如,Windows窗體異常并不是出現(xiàn)于窗體處理線程環(huán)境當中的。

原子性(數(shù)據(jù)完整性)

千萬不要讓異常影響到你數(shù)據(jù)模型的完整性。你需要保證你的對象處于比較穩(wěn)定的狀態(tài)當中——這樣一來任何由類的執(zhí)行的操作都不會出現(xiàn)違例。否則,通過“恢復(fù)”這一手段會使你的代碼變得更加讓人不解,也容易造成進一步的損壞。

考慮幾種修改私有域順序的方法。如果在修改順序的過程當中出現(xiàn)了異常,那么你的對象可能并不處于非法狀態(tài)下。嘗試在實際更新域之前去得到新的值,這樣你就可以在異常安全管理下,正常地更新你的域。

對特定類型的值——包括布爾型,32bit或者更小的數(shù)據(jù)類型與引用型——進行可變量的分配,確??梢允窃有汀]有什么保障是給一些大型數(shù)據(jù)(double,long,decimal)使用的??梢远嗫紤]這個:在共享多線程的變量時,多使用lock statements。

7.事件

事件與委托共同提供了一種關(guān)于類的方法,這種方法在有特殊的事情發(fā)生時向用戶進行提醒。委托事件的值在事件發(fā)生時應(yīng)被調(diào)用。事件就像是委托類型的域,當對象生成時,其自動初始化為null。

事件也像值為“組播”的域。這也就是說,一種委托可以依次調(diào)用其他委托。你可以將一個委托分配給一個事件,你也可以通過類似-=于+=這樣的操作來控制事件。

注意資源競爭

如果一個事件被多個線程所共享,另一個線程就有可能在你檢查是否為null之后,在調(diào)用其之前而清除所有的用戶信息——并拋出一個NullReferenceException。

對于此類問題的標準解決方法是創(chuàng)建一個該事件的副本,用于測試與調(diào)用。你仍然需要注意的是,如果委托沒有被正確調(diào)用的話,那么在其他線程里被移除的用戶仍然可以繼續(xù)操作。你也可以用某種方法將操作按順序鎖定,以避免一些問題。

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());
}

更多關(guān)于事件與競爭的信息請參閱:

http://blogs.msdn.com/b/ericlippert/archive/2009/04/29/events-and-races.aspx

不要忘記將事件處理器Unhook

使用一種事件處理器為事件資源生成一個由處理器的資源對象到接收對象的引用,可以保護接收端的garbage collection。

適當?shù)膗nhook處理器可以確保你不必因委托不再工作而去調(diào)用它浪費時間,也不會使內(nèi)存存儲無用委托與不可引用的對象。

8.屬性

屬性提供了一種向程序集、類與其信息屬性中注入元數(shù)據(jù)的方法。它們經(jīng)常用來提供信息給代碼的消費者——比如debugger、框架測試、應(yīng)用——通過反射這一方式。你也可以向你的用戶定義屬性,或是使用預(yù)定義屬性,詳見下表:

屬性 使用對象 目的
DebuggerDisplay Debugger Debugger display 格式
InternalsVisibleTo Member access 使用特定類來暴露內(nèi)部成員去指定其他的類。基于此方法,測試方法可以用來保護成員,并且persistence層可以用一些特殊的隱蔽方法。
DefaultValue Properties 為屬性指定一個缺省值

一定要對DebuggerStepThrough多重視幾分——否則它會在這個方法應(yīng)用的地方讓尋找bug變得十分困難,你也會因此而跳過某步或是推倒而重做它。

9.Debug

Debug是在開發(fā)過程中必不可少的部分。除了使運行環(huán)境不透明的部分變得可視化之外,debugger也可以侵入運行環(huán)境,并且如果不使用debugger的話會導(dǎo)致應(yīng)用程序變現(xiàn)有所不同。

使異常棧可視化

為了觀察當前框架異常狀態(tài),你可以將“$exception”這一表達添加進Visual Studio Watch窗口。這種變量包含了當前異常狀態(tài),類似于你在catch block中所看見的,但其中不包含在debugger中看見的不是代碼中的真正存在的異常。

注意訪問器的副作用

如果你的屬性有副作用,那么考慮你是否應(yīng)使用特性或者是debugger設(shè)置去避免debugger自動地調(diào)用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結(jié)束了remainingAccesses,你會發(fā)現(xiàn)它的值會變成9.這樣一來debugger的屬性值表現(xiàn)改變了你的對象的狀態(tài)。

10.性能優(yōu)化

早做計劃,不斷監(jiān)測,后做優(yōu)化

在設(shè)計階段,制定切實可行的目標。在開發(fā)階段,專注于代碼的正確性要比去做微調(diào)整有意義的多。對于你的目標,你要在開發(fā)過程中多進行監(jiān)測。只需要在你沒有達到預(yù)期的目標的時候,你才應(yīng)該去花時間對程序做一個調(diào)整。

請記住用合適的工具來確保性能的經(jīng)驗性測量,并且使測試處于這樣一種環(huán)境當中:可反復(fù)多次測試,并且測試過程盡量與現(xiàn)實當中用戶的使用習慣一致。

當你對性能進行測試的時候,一定要注意你真正所關(guān)心的測試目標是什么。在進行某一項功能的測試時,你的測試有沒有包含這項功能的調(diào)用或者是回路構(gòu)造的開銷?

我們都聽說過很多比別人做得快很多的項目神話,不要盲目相信這些,試驗與測試才是實在的東西。

由于CLR優(yōu)化的原因,有時候看起來效率不高的代碼可能會比看起來效率高的代碼運行的更快。例如,CLR優(yōu)化循環(huán)覆蓋了一個完整的數(shù)組,以避免在不可見的per-element范圍里的檢查。開發(fā)者經(jīng)常在循環(huán)一個數(shù)組之前先計算一下它的長度:

int[] a_val = int[4000];
int len = a_val.Length;
for (int i = 0; i < len; i++)
    a_val[i] = i;

通過將長度存儲進一個變量當中,CLR會不去識別這一部分,并且跳過優(yōu)化。但是有時手動優(yōu)化會反人類地導(dǎo)致更糟糕的性能表現(xiàn)。

構(gòu)造字符串

如果你打算將大量的字符串進行連接,可以使用System.Text.StringBuilder來避免生成大量的臨時字符串。

對集合使用批量處理

如果你打算生成并填滿集合中已知的大量數(shù)據(jù),由于再分配的存在,可以用保留空間來解決生成集合的性能與資源問題。你可以用AddRange方法來進一步對性能進行優(yōu)化,如下在List<T>中處理:

Persons.AddRange(listBox.Items);

11.資源管理

垃圾收集器(garbage collector)可以自動地清理內(nèi)存。即使這樣,一切被拋棄的資源也需要適當?shù)奶幚怼貏e是那些垃圾收集器不能管理的資源。

資源管理問題的常見來源
內(nèi)存碎片 如果沒有足夠大的連續(xù)的虛擬地址存儲空間,可能會導(dǎo)致分配失敗
進程限制 進程通常都可以讀取內(nèi)存的所有子集,以及系統(tǒng)可用的資源。
資源泄露 垃圾收集器只管理內(nèi)存,其他資源需要由應(yīng)用程序正確管理。
不穩(wěn)定資源 那些依賴于垃圾收集器與終結(jié)器(finalizers)的資源在很久沒用過的時候,不可被立即調(diào)用。實際上它們可能永遠不可能被調(diào)用。

利用try/finally block來確保資源已被合理釋放,或是讓你的類使用IDisposable,以及更方便更安全的聲明方式。

using (StreamReader reader=new StreamReader(file)) 
{ 
 //your code here

在產(chǎn)品代碼中避免garbage collector

除了用調(diào)用GC.Collect()干擾garbage collector之外,也可以考慮適當?shù)蒯尫呕蚴菕仐壻Y源。在進行性能測試時,如果你可以承擔這種影響帶來的后果,你再去使用garbage collector。

避免編寫finalizers

與當前一些流傳的謠言不同的是,你的類不需要Finalizers,而這只是因為IDisposable的存在!你可以讓IDisposable賦予你的類在任何已擁有的組合實例中調(diào)用Dispose的能力,但是finalizers只能在擁有未管理的資源類中使用。

Finalizers主要對交互式Win32位句柄API有很大作用,并且SafeHandle句柄是很容易利用的。

不要總是設(shè)想你的finalizers(總是在finalizer線程上運行的)會很好地與其他對象進行交互。那些其他的對象可能在該進程之前就被終止掉了。

12.并發(fā)性

處理并發(fā)性與多線程編程是件復(fù)雜的、困難的事情。在將并發(fā)性添加進你的程序之前,請確保你已經(jīng)明確了解你的做的是什么——因為這里面有太多門道了!

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

  • Async-await/Task Parallel Library/Lazy<T>


  • Lock/monitor/AutoResetEvent


  • Interlocked/Semaphore


  • 可變域與顯式barrier

以上的這些很難解釋清楚C#/.NET的復(fù)雜之處。如果你想開發(fā)一個正常的并發(fā)應(yīng)用,可以去參閱O’Reilly的《Concurrency in C# Cookboo》。

使用Volatile

將一個域標記為“volatile”是一種高級特性,而這種設(shè)置也經(jīng)常被專家所誤解。C#的編譯器會保證目標域可以被獲取與釋放語義,但是被lock的域就不適用于這種情況。如果你不知道獲取什么,不知道釋放什么語義,以及它們是怎樣影響CPU層次的優(yōu)化,那么久避免使用volatile域。取而代之的可以用更高層次的工具,比如Task Parallel Library或是CancellationToken。

線程安全與內(nèi)置方法

標準庫類型常提供使對象線程安全更容易的方法。例如Dictionary.TryGetValue()。使用此類方法一般可以使你的代碼變得更加清爽,并且你也不必擔心像TOCTOU(time-of-check-time-of-use競爭危害的一種)這樣的數(shù)據(jù)競爭。

不要鎖住“this”、字符串,或是其他普通public的對象

當使用在多線程環(huán)境下的一些類時,多注意lock的使用。鎖住字符串常量,或是其他公共對象,會阻止你鎖狀態(tài)下的封裝,還可能會導(dǎo)致死鎖。你需要阻止其他代碼鎖定在同一使用的對象上,當然你最好的選擇是使用private對象成員項。

13.避免常見的錯誤

Null

濫用null是一種常見的導(dǎo)致程序錯誤的來源,這種非正常操作可能會使程序崩潰或是其他的異常。如果你試圖獲取一個null的引用,就好像它是某對象的有效引用值(例如通過獲取一個屬性或是方法),那么在運行時就會拋出一個NullReferenceException。

靜態(tài)與動態(tài)分析工具可以在你發(fā)布代碼之前為你檢查出潛在的NullReferenceException。在C#當中,引用型為null通常是由于變量沒有引用到某個對象而造成的。對于值可為空的類型與引用型來說,是可以使用null的。例如:Nullable<Int>,空委托,已注銷的事件,“as”轉(zhuǎn)化失敗的,以及一些其他的情況。

每個null引用異常都是一個bug。相比于找到NullReferenceException這個問題來說,不如嘗試在你使用該對象之前去為null進行測試。這樣一來可以使代碼更易于最小化的try/catch block讀取。

當從數(shù)據(jù)庫表中讀取數(shù)據(jù)時,注意缺失值可以表示為DBNull 對象,而不是作為空引用。不要期望它們表現(xiàn)得像潛在的空引用一樣。

用二進制的數(shù)字表示十進制的值

Float與double都可以表示十進制實數(shù),但不能表示二進制實數(shù),并且在存儲十進制值的時候可以在必要時用二進制的近似值存儲。從十進制的角度來看,這些二進制的近似值通常都有不同的精度與取舍,有時在算數(shù)操作當中會導(dǎo)致一些不期望的結(jié)果。由于浮點型運算通常在硬件當中執(zhí)行,因此硬件條件的不可預(yù)測會使這些差異更加復(fù)雜。

在十進制精度很重要的時候,就要使用十進制了——比如經(jīng)濟方面的計算。

調(diào)整結(jié)構(gòu)

有一種常見的錯誤就是忘記了結(jié)構(gòu)是值類型,意即其復(fù)制與通過值傳遞。例如你可能見過這樣的代碼:

struct P { public int x; public int y; }
void M()
{
   P p = whatever;
   …
   p.x = something;
   …
   N(p);

忽然某一天,代碼維護人員決定將代碼重構(gòu)成這樣:

void M()
{
   P p = whatever;
   Helper(p);
   N(p);
}
void Helper(P p)
{ 
   …
   p.x = something;

現(xiàn)在當N(p)在M()中被調(diào)用,p就有了一個錯誤的值。調(diào)用Helper(p)傳遞p的副本,并不是引用p,于是在Helper()中的突變便丟失掉了。如果被正常調(diào)用,那么Helper應(yīng)該傳遞的是調(diào)整過的p的副本。

非預(yù)期計算

C#編譯器可以保護在運算過程中的常量溢出,但不一定是計算值。使用“checked”與“unchecked”兩個關(guān)鍵詞來標記你想對變量進行什么操作。

不保存返回值

與結(jié)構(gòu)體不同的是,類是引用類型,并且可以適當?shù)匦薷囊脤ο?。然而并不是所有的對象方法都可以實際修改引用對象,有一些返回的是一個新的對象。當開發(fā)者調(diào)用后者時,他們需要記住將返回值分配給一個變量,這樣才可以使用修改過的對象。在代碼審查階段,這些問題的類型通常會逃過審查而不被發(fā)現(xiàn)。像字符串之類的對象,它們是不可變的,因此永遠不可能修改這些對象。即便如此,開發(fā)者還是很容易忘記這些問題。

例如,看如下 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

如果你運行了這個代碼,那么它一在下一項的集合中進行循環(huán),你就會得到一個異常。

正確的處理方法是使用第二個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));

屬性名稱錯誤

在實現(xiàn)屬性時,要注意屬性的名稱和在類當中用的成員項的名字有很大差別。很容易在不知情的情況下使用了相同的名稱,并且在屬性被獲取的時候還會觸發(fā)死循環(huán)。

// The following code will trigger infinite recursion
private string name;
public string Name
{
    get
    {
        return Name;  // should reference “name” instead.

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

以上是“C#開發(fā)者需要注意什么”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對大家有所幫助,如果還想學(xué)習更多知識,歡迎關(guān)注億速云行業(yè)資訊頻道!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI