從CLR的角度看,只有值類型和引用類型兩種類型,但是從框架設計的角度我們把類型從邏輯上分了更多的組。如下所示:
類是引用類型的一般情況,占了框架中的大多情況,類的流行歸於它支持面向對象的特征,以及它的普遍的適用性,基類和抽象類是兩個特殊的邏輯分組,它們與擴張性有關。
由於CLR不支持多繼承,接口類型可以用來模擬多繼承,既能被引用類型實現,也能被值類型實現。
結構是值類型的一般情況,應該用於小而簡單的類型,就像編程語言的基本類型一樣。
枚舉是值類型的一個特例,它用來定義一小組值。
靜態類是那些用來容納靜態成員的類型,常用來提供對其他操作的快速訪問。
委托、異常、Attribute、數據、集合都是引用類型的特例,各有各自的用途。
在設計大型框架之前,應該決定如何將功能劃分到一組功能域中,這些功能域由名字空間表示,為了確保一組有條理的名字空間包含的類型能很好的集成,不發生沖突,以及不會重復,自頂向下的設計很有必要。導致了下面的規范:
要用名字空間把類型組織成一個相關的特性域的層次結構。(主要是為了把類型組織成一個有條理、易於浏覽的、易於理解的層次結構)
避免非常深的名字空間層次(難於浏覽,需要經常回溯)
避免有太多的名字空間。
避免把為高級場景而設計的類型和常見的編程任務而設計的類型放在同一個名字空間中。(方便用戶更容易理解框架的基本概念,容易在常見的場景中使用框架)
不要指定名字空間就定義類型。(把相關的類型組織到一個層次結構中,有助於解決可能存在的名字沖突)
標准子名字空間的命名
很少使用的類型應該放在子名字空間中,以免擾亂主名字空間,我們確定了幾組類型,應該把它們從主名字空間中區分離出來。
1. .Design子名字空間
僅用於設計時的類型應該放在名為.Design的子名字空間。如:System.Windows.Forms.Design;
System.Messaging.Design;
要用帶“.Design”後綴的名字空間來容納那些為基本名字空間提供設計時的功能的類型。
2. .Permissions子名字空間
許可類型應該放在名為“.Permissions”子名字空間中。
要用帶“.Permissions”後綴的名字空間來容納那些基本名字空間提供自定義許可的類型。
3. .Interop子命名空間
許多框架需要支持與舊系統的互操作性(interoperability)。
要用帶“.Interop”後綴的名字空間來容納那些為基本名字空間提供相互操作功能的類型。
要用帶“.Interop”後綴的名字空間來容納所有位於PIA中的代碼。
引用類型在堆上分配,由垃圾收集器管理;而值類型要麼在棧上分配並在棧展開時釋放,要麼內聯在容納它的類型中並在容納它的類型被釋放時釋放。因此,與引用類型的分配與釋放相比,值類型的分配與釋放開銷更低。
引用類型的數組不是非內聯分配的,意為數組元素只是一些引用,指向那些位於堆中的引用類型的實例。而值類型的分配是內聯的,數組的元素就是值類型的真正實例。因此值類型的分配和釋放的開銷要比引用類型的大的多,在大多情況下,值類型數組具有更好的局部性。
值類型在被強制轉換為對象或裝箱因為裝箱和對象是在堆上分配的,且由垃圾收集器管理,所以太多的裝拆箱操作會對堆、垃圾收集器,並對系統性能造成影響。
引用類型的賦值是復制引用,而值類型的賦值復制整個值,對大的引用類型復制開銷要比值類型小的多。
引用類型是引用傳遞,值類型是值傳遞。改變引用類型的一個實例會影響其他的實例,改變值類型的實例,不會影響到它的副本。
框架中的大多數類型應該是類,但是在某些特殊情況下,由於值類型所具有的特征,使用結構更合適。
考慮定義結構而不是類(如果該類型的實例比較小,生命周期比較短,經常被內嵌在其他對象中)
不要定義結構,除非該類型具有以下特征:
它在邏輯上代表一個獨立的值,與基本類型相似(int)
它的實例大小小於16字節
它是不可變的
它不需要被經常裝箱
在所有的其他情況下,應該將類型定義為類。
一般來說,類是用來暴漏抽象的優先選擇。
點在於當需要允許API不斷演化時,它的靈活性不如類,一旦你發布了一個接口,它的成員就永遠固定了,給接口添加任何東西都會破壞已經實現該接口的已有類型。
類提供了更多的靈活性,你可以給一個已發布的類添加成員。只要添加的方法不是抽象的,任何已有的派生類無需改變仍能繼續使用。
要優先采用類而不是接口
與基於接口的API相比,基於類的API容易演化得多,因為可以給類型添加成員而不會破壞已有的代碼。
要用抽象類而不是接口來解除協定與實現之間的耦合。
抽象類經過正確的設計,同樣能夠解除協定與實現之間的耦合,與接口能達到的程度不相上下。
要定義接口,如果需要提供一個多態的值類型層次結構的話。(值類型不能自其它類型繼承,但是她們可以實現接口)
考慮通過定義接口來達到與多重繼承相類似的效果。
不要在抽象類型中定義公有的或內部受保護的構造函數。
只有當用戶需要創建一個類型的實例時,該類型的構造參數才是公有的,由於你無法創建一個抽象類的實例,因此如果抽象類型具有公有構造函數
要為抽象類定義受保護的構造函數或內部構造函數。
受保護的構造函數僅僅是允許子類型被創建時,基類能夠做自己的初始化。
內部構造函數可以用來把該抽象類的具體實現限制在定義該抽象類的程序集中。
要為發布的抽象類提供至少一個繼承自該類的具體類型。
有助於驗證該抽象類的設計是否正確。
靜態類被定義為一個只包含靜態成員的類。如果一個類被定義為靜態,那麼它就是密封的、抽象的,不能覆蓋或者聲明任何實例成員。
靜態類是在純面向對象設計和簡單性之間的一個權衡,它們被廣泛用來提供一下訪問其他操作的快捷方式,或者不需要完整的面向對象封裝器的時候提供一些功能。(System.Enviroment)
要盡量少用靜態類
靜態類僅被用作輔助類,來支持框架的面向對象的核心。
不要把靜態類當做雜物箱。
每一個靜態類都應該有其明確的目的。
不要在靜態類中聲明或覆蓋實例成員。
要把靜態類定義為密封的、抽象的,並添加一個私有的實例構造函數。
雖然大多數情況下API用類或結構來構建最好,但是在有些情況下,接口更合適。
CLR 不支持多繼承,但允許類型實現一個或多個接口,因此通常用接口來實現多繼承。
在創建能夠為多種類型(包括值類型)所支持的公共接口時。
要定義接口,如果你需要包括值類型在內的一組類型支持一些公共的API。
考慮定義接口,如果需要讓已經自其它類型繼承的類型支持該接口提供的功能。
避免使用記號接口(沒有成員的接口)
要為接口提供至少一個實現該接口的類型。
要為你定義的每個接口提供至少一個使用該接口的API(一個以接口為參數的方法或是一個類型為該接口的屬性)
不要給已發行的接口再添加成員。
這樣做會破壞該接口的實現,為了避免版本的問題,應該創建一個新的接口。
一般來說,在為托管代碼設計可重用的程序庫時,你應該選擇類而不是接口。
通用目的的值類型通常稱為struct(結構)。
不要為結構提供默認的構造函數。(C#不允許結構有默認的構造函數)
要確保所有的實例數據都為零,false,或null時,結構仍處於有效狀態。(可以防止在創建一個結構時創建出無效的實例)
要為值類型實現IEquatable<T>
值類型的Object.Equals方法會導致裝箱,默認的實現並不高效,因為使用了反射,IEquatable<T>.Equals性能好的多,不會導致裝箱。
不要顯示的擴展System.ValueType
枚舉是一種特殊的值類型,有兩種類型的枚舉:簡單枚舉和標記枚舉。
簡單枚舉代表小型的、閉合的一組選擇。例如(一組顏色):
Public enumColor{
Red,
Green,
Blue,
……
}
標記枚舉的設計是為了支持對枚舉值進行按位操作。標記枚舉的常見例子是一個選擇列表
[Flags]
Public enumAttributeTargets
{
Assembly=0x0001,
Module=0x0002,
Cass=0x0004,
Struct=0x0008
}
要用枚舉來加強那些表示值的集合的參數、屬性以及返回值的類型性。
要優先使用枚舉而不要使用靜態常量。(枚舉是一個包含一組靜態常量的結構)
不要把枚舉用於開發的集合(比如操作系統版本等)
不要提供為了今後使用而保留的枚舉值。
避免顯示的暴漏只有一個值的枚舉。
不要把sentinel值包含在枚舉值中。
要為簡單枚舉類型提供零值。(應該考慮把該值稱為None之類的東西,如果這樣的值不適合用於某個特定的枚舉,那麼應該把該枚舉中最常用的默認值賦值為0)
考慮用Int32作為枚舉的基本實現類型。
要用復數名詞或者名詞短語來命名標記枚舉,用單數名詞或者名詞短語來命名簡單枚舉。
不要直接擴充System.Enum
要對標記枚舉使用System.FlagsAttribute,不要把該attribute用於簡單枚舉。
[Flags]
Public enumAttributeTargets
{
…..
}
要用2 的冪次方作為標記枚舉的值,這樣就可以通過按位或操作自由組合他們。
[Flags]
Public enumWatcherChangeTypes
{
Created=0x0002,
Deleted=0x0004,
Changed=0x0008,
Renamed=0x00010,
}
考慮為常用的標記組合提供特殊的枚舉值。(位操作是一個高級概念,對應簡單任務來說不是必須的,FileAccess.ReadWrite就是這樣一個例子)
[Flags]
Public enumFileAccess
{
Read=1,
Write=2,
ReadWrite=Read|Write,
}
避免讓創建的標記枚舉包含某些無效的組合。
避免把零用作標記枚舉的值,除非該值表示“所有標記都被清除“,而且按下一條規范進行了適當的命名。(CLR規定任何值類型的默認值“所有的位都清零“)
要把標記枚舉的零值命名為None,對其標枚舉來說,該值必須始終意味著“所有標記均被清除”。
[Flags]
Public enumBorderStyle
{
Fixed3D=0x1,
FixedSingle=0x2,
None=0x0
}
但是,該規則只適用於標記枚舉,對於非標記枚舉的情況,避免使用0值實際上是不利的,所有的枚舉類型一開始都為零值。
常會發現在發現之後需要給一個枚舉添加值,如果新添加的值是一個已有API的返回值,那麼就存在潛在的應用程序兼容性問題。
考慮給枚舉添加值,盡管有那麼一點兼容性的風險。
如果有實際數據,表明給枚舉添加值會導致應用程序的不兼容,可以考慮添加一個新的API來返回新老枚舉值,這樣就能確保仍然兼容現有的應用程序。
嵌套類型是一個定義在另一個類型的作用域內的類型。另一個類型被稱為外層類型。嵌套類型能夠訪問外層類型的所有成員。可以訪問定義在外層類型的私有字段以及定義在外層類型的所有父類的受保護字段。
一般來說,盡量少用嵌套類型,嵌套類型與外層類型緊密耦合,不適合將它們作為通用類型。嵌套類型適合用來對它們的外層類型的實現細節建模。
要想讓一個類型能夠訪問外層類型的成員時才使用嵌套類型。
不要用嵌套類型進行邏輯分組,應該用名字空間來達到此目的。
避免公開的暴漏嵌套類型,唯一的例外是如果只需要在極少數的場景中聲明嵌套類型的變量,比如派生子類,或者其他高級自定義場景中。
不要使用嵌套類型,如果該類型可能會被除了它的外層類型之外的類型引用。
不要使用嵌套類型,如果它們需要被客戶代碼實例化。
不要把嵌套類型定義為接口的成員。
一般來說盡量少用嵌套類型,而且應該避免將嵌套類型公開暴漏給外界。