【《Effective C#》提炼总结】提高Unity中C#代码质量的21条准则
原則1 ??盡可能地使用屬性而不是可直接訪問的數(shù)據(jù)成員
? ? ? ?
●?屬性(property)一直是C#語言中比較有特點的存在。屬性允許將數(shù)據(jù)成員作為共有接口的一部分暴露出去,同時仍舊提供面向?qū)ο蟓h(huán)境下所需的封裝。屬性這個語言元素可以讓你像訪問數(shù)據(jù)成員一樣使用,但其底層依舊是使用方法實現(xiàn)的。
?
●?使用屬性,可以非常輕松的在get和set代碼段中加入檢查機制。
?
需要注意,正因為屬性是用方法實現(xiàn)的,所以它擁有方法所擁有的一切語言特性:
1)屬性增加多線程的支持是非常方便的。你可以加強 get 和 set 訪問器(accessors)的實現(xiàn)來提供數(shù)據(jù)訪問的同步。
2)屬性可以被定義為virtual。
3)可以把屬性擴展為abstract。
4)可以使用泛型版本的屬性類型。
5)屬性也可以定義為接口。
6)因為實現(xiàn)實現(xiàn)訪問的方法get與set是獨立的兩個方法,在C# 2.0之后,你可以給它們定義不同的訪問權(quán)限,來更好的控制類成員的可見性。
7)而為了和多維數(shù)組保持一致,我們可以創(chuàng)建多維索引器,在不同的維度上使用相同或不同類型。
?
無論何時,需要在類型的公有或保護(hù)接口中暴露數(shù)據(jù),都應(yīng)該使用屬性。如果可以也應(yīng)該使用索引器來暴露序列或字典。現(xiàn)在多投入一點時間使用屬性,換來的是今后維護(hù)時的更加游刃有余。
?
?
原則2 ??偏向于使用運行時常量而不是編譯時常量
? ? ? ?
對于常量,C#里有兩個不同的版本:運行時常量(readonly)和編譯時常量(const)。
?
應(yīng)該盡量使用運行時常量,而不是編譯器常量。雖然編譯器常量略快,但并沒有運行時常量那么靈活。應(yīng)僅僅在那些性能異常敏感,且常量的值在各個版本之間絕對不會變化時,再使用編譯時常量。
?
編譯時常量與運行時常量不同之處表現(xiàn)在于他們的訪問方式不同,因為Readonly值是運行時解析的:
●?編譯時常量(const)的值會被目標(biāo)代碼中的值直接取代。
●?運行時常量(readonly)的值是在運行時進(jìn)行求值。●?引用運行時生成的IL將引用到readonly變量,而不是變量的值。
?
這個差別就帶來了如下規(guī)則:
●?編譯時常量(const)僅能用于數(shù)值和字符串。
●?運行時常量(readonly)可以為任意類型。運行時常量必須在構(gòu)造函數(shù)或初始化器中初始化,因為在構(gòu)造函數(shù)執(zhí)行后不能再被修改。你可以讓某個readonly值為一個DataTime結(jié)構(gòu),而不能指定某個const為DataTIme。
●?可以用readonly值保存實例常量,為類的每個實例存放不同的值。而編譯時常量就是靜態(tài)的常量。
●?有時候你需要讓某個值在編譯時才確定,就最好是使用運行時常量(readonly)。
●?標(biāo)記版本號的值就應(yīng)該使用運行時常量,因為它的值會隨著每個不同版本的發(fā)布而改變。
●?const優(yōu)于readonly的地方僅僅是性能,使用已知的常量值要比訪問readonly值略高一點,不過這其中的效率提升,可以說是微乎其微的。
?
綜上,在編譯器必須得到確定數(shù)值時,一定要使用const。例如特性(attribute)的參數(shù)和枚舉的定義,還有那些在各個版本發(fā)布之間不會變化的值。除此之外的所有情況,都應(yīng)盡量選擇更加靈活的readonly常量。
?
?
原則3 ??推薦使用is 或as操作符而不是強制類型轉(zhuǎn)換
? ? ? ? ? ??
●?C#中,is和as操作符的用法概括如下:
is :?檢查一個對象是否兼容于其他指定的類型,并返回一個Bool值,永遠(yuǎn)不會拋出異常。
as:作用與強制類型轉(zhuǎn)換是一樣,但是永遠(yuǎn)不會拋出異常,即如果轉(zhuǎn)換不成功,會返回null。
?
●?盡可能的使用as操作符,因為相對于強制類型轉(zhuǎn)換來說,as更加安全,也更加高效。
?
●?as在轉(zhuǎn)換失敗時會返回null,在轉(zhuǎn)換對象是null時也會返回null,所以使用as進(jìn)行轉(zhuǎn)換時,只需檢查返回的引用是否為null即可。
?
●?as和is操作符都不會執(zhí)行任何用戶自定義的轉(zhuǎn)換,它們僅當(dāng)運行時類型符合目標(biāo)類型時才能轉(zhuǎn)換成功,也不會在轉(zhuǎn)換時創(chuàng)建新的對象。
?
●?as運算符對值類型是無效,此時可以使用is,配合強制類型轉(zhuǎn)換進(jìn)行轉(zhuǎn)換。
?
●?僅當(dāng)不能使用as進(jìn)行轉(zhuǎn)換時,才應(yīng)該使用is操作符。否則is就是多余的。
?
原則4 ??推薦使用條件屬性而不是#if條件編譯
? ? ? ? ? ??
●?由于#if/#endif很容易被濫用,使得編寫的代碼難于理解且更難于調(diào)試。C#為此提供了一條件特性(Conditional attribute)。使用條件特性可以將函數(shù)拆分出來,讓其只有在定義了某些環(huán)境變量或設(shè)置了某個值之后才能編譯并成為類的一部分。Conditional特性最常用的地方就是將一段代碼變成調(diào)試語句。
?
●?Conditional特性只可應(yīng)用在整個方法上,另外,任何一個使用Conditional特性的方法都只能返回void類型。不能再方法內(nèi)的代碼塊上應(yīng)用Conditional特性。也不可以在有返回值的方法上應(yīng)用Conditional特性。但應(yīng)用了Conditional特性的方法可以接受任意數(shù)目的引用類型參數(shù)。
?
●?使用Conditional特性生成的IL要比使用#if/#Eendif時更有效率。同時,將其限制在函數(shù)層面上可以更加清晰地將條件性的代碼分離出來,以便進(jìn)一步保證代碼的良好結(jié)構(gòu)。
?
?
原則5 ??理解幾個等同性判斷之間的關(guān)系
●?C#中可以創(chuàng)建兩種類型:值類型和引用類型。如果兩個引用類型的變量指向的是同一個對象,它們將被認(rèn)為是“引用相等”。如果兩個值類型的變量類型相同,而且包含同樣的內(nèi)容,它們被認(rèn)為是“值相等”。這也是等同性判斷需要如此多方法的原因。
?
●?當(dāng)我們創(chuàng)建自己的類型時(無論是類還是struct),應(yīng)為類型定義“等同性”的含義。C#提供了4種不同的函數(shù)來判斷兩個對象是否“相等”。
1)public static bool ReferenceEquals (object left, object right);判斷兩個不同變量的對象標(biāo)識(object identity)是否相等。無論比較的是引用類型還是值類型,該方法判斷的依據(jù)都是對象標(biāo)識,而不是對象內(nèi)容。
2)public static bool Equals (object left, object right); 用于判斷兩個變量的運行時類型是否相等。
3)public virtual bool Equals(object right); 用于重載
4)public static bool operator ==(MyClass left, MyClass right); 用于重載
?
●?不應(yīng)該覆寫Object.referenceEquals()靜態(tài)方法和Object.Equals()靜態(tài)方法,因為它們已經(jīng)完美的完成了所需要完成的工作,提供了正確的判斷,并且該判斷與運行時的具體類型無關(guān)。對于值類型,我們應(yīng)該總是覆寫Object.Equals()實例方法和operatior==( ),以便為其提供效率更高的等同性判斷。對于引用類型,僅當(dāng)你認(rèn)為相等的含義并非是對象標(biāo)識相等時,才需要覆寫Object.Equals( )實例方法。在覆寫Equals( )時也要實現(xiàn)IEquatable<T>。
?
PS: 此原則對應(yīng)于《EffectiveC# Second Edition》中原則6。
?
?
原則6 ??了解GetHashCode( )的一些坑
●?GetHashCode( )方法在使用時會有不少坑,要謹(jǐn)慎使用。GetHashCode()函數(shù)僅會在一個地方用到,即為基于散列(hash)的集合定義鍵的散列值時,此類集合包括HashSet<T>和Dictionary<K,V>容器等。對引用類型來講,索然可以正常工作,但是效率很低。對值類型來講,基類中的實現(xiàn)有時甚至不正確。而且,編寫的自己GetHashCode( )也不可能既有效率又正確。
?
●?在.NET中,每個對象都有一個散列碼,其值由System.Object.GetHashCode()決定。
?
●?實現(xiàn)自己的GetHashCode( )時,要遵循上述三條原則:
1)如果兩個對象相等(由operation==定義),那么他們必須生成相同的散列碼。否則,這樣的散列碼將無法用來查找容器中的對象。
2)對于任何一個對象A,A.GetHashCode()必須保持不變。
3)對于所有的輸入,散列函數(shù)應(yīng)該在所有整數(shù)中按隨機分別生成散列碼。這樣散列容器才能得到足夠的效率提升。
?
PS: 此原則對應(yīng)于《EffectiveC# Second Edition》中原則7。
?
?
原則7 ??理解短小方法的優(yōu)勢
將C#代碼翻譯成可執(zhí)行的機器碼需要兩個步驟。
C#編譯器將生成IL,并放在程序集中。隨后,JIT將根據(jù)需要逐一為方法(或是一組方法,如果涉及內(nèi)聯(lián))生成機器碼。短小的方法讓JIT編譯器能夠更好地平攤編譯的代價。短小的方法也更適合內(nèi)聯(lián)。
?
除了短小之外,簡化控制流程也很重要。控制分支越少,JIT編譯器也會越容易地找到最適合放在寄存器中的變量。
?
所以,短小方法的優(yōu)勢,并不僅體現(xiàn)在代碼的可讀性上,還關(guān)系到程序運行時的效率。——MyPS:我認(rèn)為這一條對于Python等其他語言也適用
?
PS:此原則對應(yīng)于《EffectiveC# Second Edition》中原則11。
?
?
原則8 ?選擇變量初始化而不是賦值語句
成員初始化器是保證類型中成員均被初始化的最簡單的方法——無論調(diào)用的是哪一個構(gòu)造函數(shù)。初始化器將在所有構(gòu)造函數(shù)執(zhí)行之前執(zhí)行。使用這種語法也就保證了你不會再添加的新的構(gòu)造函數(shù)時遺漏掉重要的初始化代碼。
?
綜上,若是所有的構(gòu)造函數(shù)都要將某個成員變量初始化成同一個值,那么應(yīng)該使用初始化器。(MyPS:在構(gòu)造器中處理,本質(zhì)上是對成員變量(即字段)進(jìn)行賦值——因為使用的是賦值語句;我覺得,如果和構(gòu)造器參數(shù)無關(guān),那么直接在定義時初始化就好)
?
PS: 此原則對應(yīng)于《Effective C# Second Edition》中原則12。
?
?
原則9 ??正確地初始化靜態(tài)成員變量
●?C#提供了有靜態(tài)初始化器和靜態(tài)構(gòu)造函數(shù)來專門用于靜態(tài)成員變量的初始化。
?
●?靜態(tài)構(gòu)造函數(shù)是一個特殊的函數(shù),將在其他所有方法執(zhí)行之前以及變量或?qū)傩员坏谝淮卧L問之前執(zhí)行。可以用這個函數(shù)來初始化靜態(tài)變量,實現(xiàn)單例模式或執(zhí)行類可用之前必須進(jìn)行的任何操作。
?
●?和實例初始化一樣,也可以使用初始化器語法來替代靜態(tài)的構(gòu)造函數(shù)。若只是需要為某個靜態(tài)成員分配空間,那么不妨使用初始化器的語法。而若是要更復(fù)雜一些的邏輯來初始化靜態(tài)成員變量,那么可以使用靜態(tài)構(gòu)造函數(shù)。
?
●?使用靜態(tài)構(gòu)造函數(shù)而不是靜態(tài)初始化器最常見的理由就是處理異常。在使用靜態(tài)初始化器時,我們無法自己捕獲異常。而在靜態(tài)構(gòu)造函數(shù)中卻可以做到。
?
PS: 此原則對應(yīng)于《Effective C# Second Edition》中原則13。
?
?
原則10 ??使用構(gòu)造函數(shù)鏈(減少重復(fù)的初始化邏輯)
? ? ??
●?編寫構(gòu)造函數(shù)很多時候是個重復(fù)性的勞動,如果你發(fā)現(xiàn)多個構(gòu)造函數(shù)包含相同的邏輯,可以將這個邏輯提取到一個通用的構(gòu)造函數(shù)中。這樣既可以避免代碼重復(fù),也可以利用構(gòu)造函數(shù)初始化器來生成更高效的目標(biāo)代碼。
?
●?C#編譯器將把構(gòu)造函數(shù)初始化器看做是一種特殊的語法,并移除掉重復(fù)的變量初始化器以及重復(fù)的基類構(gòu)造函數(shù)調(diào)用。這樣使得最終的對象可以執(zhí)行最少的代碼來保證初始化的正確性。
?
●?構(gòu)造函數(shù)初始化器允許一個構(gòu)造函數(shù)去調(diào)用另一個構(gòu)造函數(shù)。而C# 4.0添加了對默認(rèn)參數(shù)的支持,這個功能也可以用來減少構(gòu)造函數(shù)中的重復(fù)代碼。你可以將某個類的所有構(gòu)造函數(shù)統(tǒng)一成一個,并為所有的可選參數(shù)指定默認(rèn)值。其他的幾個構(gòu)造函數(shù)調(diào)用某個構(gòu)造函數(shù),并提供不同的參數(shù)即可。
?
PS: 此原則對應(yīng)于《EffectiveC# Second Edition》中原則14。
?
?
原則11 ??實現(xiàn)標(biāo)準(zhǔn)的銷毀模式
●?GC可以高效地管理應(yīng)用程序使用的內(nèi)存。不過創(chuàng)建和銷毀堆上的對象仍舊需要時間。若是在某個方法中創(chuàng)建了太多的引用對象,將會對程序的性能產(chǎn)生嚴(yán)重的影響。
?
這里有一些規(guī)則,可以幫你盡量降低GC的工作量:
1)若某個引用類型(值類型無所謂)的局部變量用于被頻繁調(diào)用的例程中,那么應(yīng)該將其提升為成員變量。
2)為常用的類型實例提供靜態(tài)對象。
3)創(chuàng)建不可變類型的最終值。比如string類的+=操作符會創(chuàng)建一個新的字符串對象并返回,多次使用會產(chǎn)生大量垃圾,不推薦使用。對于簡單的字符串操作,推薦使用string.Format。對于復(fù)雜的字符串操作,推薦使用StringBuilder類。
?
PS: 此原則對應(yīng)于《EffectiveC# Second Edition》中原則16。
?
?
原則12 ?區(qū)分值類型和引用類型
●?C#中,class對應(yīng)引用類型,struct對應(yīng)值類型。
?
●?C#不是C++,不能將所有類型定義成值類型并在需要時對其創(chuàng)建引用。C#也不是Java,不像Java中那樣所有的東西都是引用類型。你必須在創(chuàng)建時就決定類型的表現(xiàn)行為,這相當(dāng)重要,因為稍后的更改可能帶來很多災(zāi)難性的問題。
?
●?值類型無法實現(xiàn)多態(tài),因此其最佳用途就是存放數(shù)據(jù)。引用類型支持多態(tài),因此用來定義應(yīng)用程序的行為。
?
●??一般情況下,我們習(xí)慣用class,隨意創(chuàng)建的大都是引用類型,若下面幾點都肯定,那么應(yīng)該創(chuàng)建struct值類型:
1)該類型主要職責(zé)在于數(shù)據(jù)存儲嗎?
2)該類型的公有接口都是由訪問其數(shù)據(jù)成員的屬性定義的嗎?
3)你確定該類型絕不會有派生類型嗎?
4)你確定該類型永遠(yuǎn)都不需要多態(tài)支持嗎?
?
●?用值類型表示底層存儲數(shù)據(jù)的類型,用引用類型來封裝程序的行為。這樣,你可以保證類暴露出的數(shù)據(jù)能以復(fù)制的形式安全提供,也能得到基于棧存儲和使用內(nèi)聯(lián)方式存儲帶來的內(nèi)存性能提升,更可以使用標(biāo)準(zhǔn)的面向?qū)ο蠹夹g(shù)來表達(dá)應(yīng)用程序的邏輯。而倘若你對類型未來的用圖不確定,那么應(yīng)該選擇引用類型。
?
PS: 此原則對應(yīng)于《Effective C# Second Edition》中原則18。
?
原則13 ?保證0為值類型的有效狀態(tài)
在創(chuàng)建自定義枚舉值時,請確保0是一個有效的選項。若你定義的是標(biāo)志(flag),那么可以將0定義為沒有選中任何狀態(tài)的標(biāo)志(比如None)。即作為標(biāo)記使用的枚舉值(即添加了Flags特性)應(yīng)該總是將None設(shè)置為0。
?
PS: 此原則對應(yīng)于《Effective C# Second Edition》中原則19。
?
?
原則14
保證值類型的常量性和原子性
常量性的類型使得我們的代碼更加易于維護(hù)。不要盲目地為類型中的每一個屬性都創(chuàng)建get和set訪問器。對于那些目的是存儲數(shù)據(jù)的類型,應(yīng)該盡可能地保證其常量性和原子性。
?
PS: 此原則對應(yīng)于《Effective C# Second Edition》中原則20。
?
原則15 ??限制類型的可見性
在保證類型可以完成其工作的前提下。你應(yīng)該盡可能地給類型分配最小的可見性。也就是,僅僅暴露那些需要暴露的。盡量使用較低可見性的類來實現(xiàn)公有接口。可見性越低,能訪問你功能的代碼越少,以后可能出現(xiàn)的修改也就越少。
?
PS: 此原則對應(yīng)于《Effective C# Second Edition》中原則21。
?
?
原則16 ?通過定義并實現(xiàn)接口替代繼承
●?理解抽象基類(abstract class)和接口(interface)的區(qū)別:
1)接口是一種契約式的設(shè)計方式,一個實現(xiàn)某個接口的類型,必須實現(xiàn)接口中約定的方法。抽象基類則為一組相關(guān)的類型提供了一個共同的抽象。也就是說抽象基類描述了對象是什么,而接口描述了對象將如何表現(xiàn)其行為。
?
2)接口不能包含實現(xiàn),也不能包含任何具體的數(shù)據(jù)成員。而抽象基類可以為派生類提供一些具體的實現(xiàn)。
?
3)基類描述并實現(xiàn)了一組相關(guān)類型間共用的行為。接口則定義了一組具有原子性的功能,供其他不相關(guān)的具體類型來實現(xiàn)。
?
●?理解好兩者之間的差別,我們便可以創(chuàng)造更富表現(xiàn)力、更能應(yīng)對變化的設(shè)計。使用類層次來定義相關(guān)的類型。用接口暴露功能,并讓不同的類型實現(xiàn)這些接口。
?
PS: 此原則對應(yīng)于《EffectiveC# Second Edition》中原則22。
?
?
原則17 ?理解接口方法和虛方法的區(qū)別
第一眼看來,實現(xiàn)接口和覆寫虛方法似乎沒有什么區(qū)別,實際上,實現(xiàn)接口和覆寫虛方法之間的差別很大。
1)接口中聲明的成員方法默認(rèn)情況下并非虛方法,所以,派生類不能覆寫基類中實現(xiàn)的非虛接口成員。若要覆寫的話,將接口方法聲明為virtual即可。
?
2)基類可以為接口中的方法提供默認(rèn)的實現(xiàn),隨后,派生類也可以聲明其實現(xiàn)了該接口,并從基類中繼承該實現(xiàn)。
?
3)實現(xiàn)接口擁有的選擇要比創(chuàng)建和覆寫虛方法多。我們可以為類層次創(chuàng)建密封(sealed)的實現(xiàn),虛實現(xiàn)或者抽象的契約。還可以創(chuàng)建密封的實現(xiàn),并在實現(xiàn)接口的方法中提供虛方法進(jìn)行調(diào)用。
?
PS: 此原則對應(yīng)于《EffectiveC# Second Edition》中原則23。
?
?
原則18 ??用委托實現(xiàn)回調(diào)
在C#中,回調(diào)是用委托來實現(xiàn)的,主要要點如下:
1)委托為我們提供了類型安全的回調(diào)定義。雖然大多數(shù)常見的委托應(yīng)用都和事件有關(guān),但這并不是C#委托應(yīng)用的全部場合。當(dāng)類之間有通信的需要,并且我們期望一種比接口所提供的更為松散的耦合機制時,委托便是最佳的選擇。
?
2)委托允許我們在運行時配置目標(biāo)并通知多個客戶對象。委托對象中包含一個方法的應(yīng)用,該方法可以是靜態(tài)方法,也可以是實例方法。也就是說,使用委托,我們可以和一個或多個在運行時聯(lián)系起來的客戶對象進(jìn)行通信。
?
3)由于回調(diào)和委托在C#中非常常用,以至于C#特地以lambda表達(dá)式的形式為其提供了精簡語法。
?
4)由于一些歷史原因,.NET中的委托都是多播委托(multicast delegate)。多播委托調(diào)用過程中,每個目標(biāo)會被依次調(diào)用。委托對象本身不會捕捉任何異常。因此,任何目標(biāo)拋出的異常都會結(jié)束委托鏈的調(diào)用。
?
PS: 此原則對應(yīng)于《EffectiveC# Second Edition》中原則24。
?
?
原則19 ?用事件模式實現(xiàn)通知
●?事件提供了一種標(biāo)準(zhǔn)的機制來通知監(jiān)聽者,而C#中的事件其實就是觀察者模式的一個語法上的快捷實現(xiàn)。
?
●?事件是一種內(nèi)建的委托,用來為事件處理函數(shù)提供類型安全的方法簽名。任意數(shù)量的客戶對象都可以將自己的處理函數(shù)注冊到事件上,然后處理這些事件,這些客戶對象無需在編譯器就給出,事件也不必非要有訂閱者才能正常工作。
?
●?在C#中使用事件可以降低發(fā)送者和可能的通知接受者之間的耦合,發(fā)送者可以完全獨立于接受者進(jìn)行開發(fā)。
?
PS: 此原則對應(yīng)于《EffectiveC# Second Edition》中原則25。
?
?
原則20 ??避免返回對內(nèi)部類對象的引用
●?若將引用類型通過公有接口暴露給外界,那么對象的使用者即可繞過我們定義的方法和屬性來更改對象的內(nèi)部結(jié)構(gòu),這會導(dǎo)致常見的錯誤。
?
●?共有四種不同的策略可以防止類型內(nèi)部的數(shù)據(jù)結(jié)構(gòu)遭到有意或無意的修改:
1)值類型。當(dāng)客戶代碼通過屬性來訪問值類型成員時,實際返回的是值類型的對象副本。
2)常量類型。如System.String。
3)定義接口。將客戶對內(nèi)部數(shù)據(jù)成員的訪問限制在一部分功能中。
4)包裝器(wrapper)。提供一個包裝器,僅暴露該包裝器,從而限制對其中對象的訪問。
?
PS: 此原則對應(yīng)于《Effective C# Second Edition》中原則26。
?
?
原則21 ??僅用new修飾符處理基類更新
●?使用new操作符修飾類成員可以重新定義繼承自基類的非虛成員。
?
●?new修飾符只是用來解決升級基類所造成的基類方法和派生類方法沖突的問題。
?
●?new操作符必須小心使用。若隨心所欲的濫用,會造成對象調(diào)用方法的二義性。
?
PS: 此原則對應(yīng)于《Effective C# Second Edition》中原則33
轉(zhuǎn)載于:https://www.cnblogs.com/changbaishan/p/8716311.html
總結(jié)
以上是生活随笔為你收集整理的【《Effective C#》提炼总结】提高Unity中C#代码质量的21条准则的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 解决每次从cmd进入sqlplus,都得
- 下一篇: C# 读写注册表