現在總算是進展到OOP最重要的類型了,感覺很興奮但又困惑,因為C#中有很多與其他OOP語言像是java大不相同的處理。
1.可見性
C#中的可見性很多與java重疊並且意義相同,但有一個特別的internal(internal其實並不特別)。internal表示僅對定義程序集(assembly)中的所有代碼可見,至於其他程序集不可見。我的第一眼感覺就是java的默認訪問權限,即包訪問權限,至少它們是類似的。事實上,CLR中的程序集的確對應包,因為它的定義就是一個或多個模塊/資源文件的邏輯性分組,而且是重用,安全性及版本控制的最小單元,這些都符合包的特點。正如包訪問權限是java的默認權限一樣,C#的默認權限就是internal。
在C#中,當我們在派生類中覆寫基類的抽象方法或虛方法時,我們不能修改它們的可見性,就算是放寬可見性也不行(在java中,這是允許的)。
2.靜態類
靜態類的唯一作用就是將一組相關成員組合到一起,其實也就是靜態成員,因為靜態類無法訪問實例成員,它並沒有this引用。我們可以將一個類聲明為靜態類,但是結構不行,雖然結構和類非常相似。靜態類直接繼承自System.Object,因為繼承自其他類是枉然的,它根本就無法創建對象實例。靜態類還不能實現任何接口,同樣也是因為只有使用對象實例時才能發揮接口的作用。靜態類中的所有成員都是靜態的,原因前面已經講過了。靜態類也不能作為字段,方法參數或局部變量使用,因為它們默認都代表引用一個實例變量。靜態類在編譯器中標記為abstract(抽象)和sealed(密封),這是值得注意的。
很少使用靜態類,像是java也比較少,因為定義一個類,更多是為了封裝數據和行為,並提供訪問接口,但靜態類根本就是一個封閉的集合,就像前面講的,它只是為了封裝靜態成員而已。
3.分部類
關鍵字partial非常神奇,第一次接觸這個關鍵字以及了解它的用途之后,我對C#的包容性真的很震驚,它似乎就是一個集合體,把很多東西都攪合在一起。
partial關鍵字告訴編譯器,這個類,結構或者接口的定義源碼可能要分散到一個或多個源代碼中文件中。也許我們會很奇怪:為什么要將我們的源碼分散到多個文件中呢?原因有以下幾個方面:
1.源代碼控制:程序員可以將一個類型的源碼從一個源碼控制系統中簽出(check out)進行修改,如果將類型的源碼分散到多個源碼文件中,就能使多名程序員同時修改單互不影響。
2.在同一個文件中,將一個類型或結構分解成不同的邏輯單元:這適合測試類型的功能,我們可以在一個源碼文件中反復聲明同一個分部類型,然后在這個類型的多個部分實現不同功能,然后測試它的運行情況。我們可以選擇注釋掉某個部分,或者是用另一個部分來代替它。
3.代碼拆分:如果熟悉ASP.Net的同學,對拆分一定很熟悉,是的,就是這個拆分。我們在ASP.Net中,經常會將某個組件放進我們的源碼中,然后我們就會發現,該組件的代碼竟然插入到我們的源碼中,如果只是插入,那還沒什么,但是我們點擊拆分窗口,就可以查看我們的源碼的運行情況。這就是利用了partial關鍵字所起到的神奇作用,不信,大家在使用ASP.Net的時候,可以看看整個源碼。
partial在組件軟件編程(Component Software Programming,CSP)發揮了很大的作用,同時它也也是OOP思想的一個極佳體現。
4.其他關鍵字
其他關鍵字像是virtual,new我們都很熟悉(對於virtual由於之前學C++的時候不扎實,所以在寫C#例程的時候犯了錯誤,以為它是java的abstract,沒有寫方法體),virtual表明該方法可以被派生類覆寫(override方法也可以,因為它本身就是一個virtual或abstract方法的覆寫版本),而且C#要求覆寫方法時必須注明override(java並不需要,但我們需要添加一個標簽@Override提醒這是基類的覆寫版本),而new在C#中卻有新的用法:
public new void Show(){} public new String name = "";
它們表明,這是派生類自己的字段和方法,而不是基類的。這種情況適合我們在派生類中定義與基類同名的方法或字段,如果沒有這個new聲明,默認采用基類的方法和字段,但編譯器會提出警告,因為同名的方法或字段肯定是為了重新定義(不同於覆寫,C#不支持非虛方法和字段的覆寫),我們完全可以直接使用基類的方法和字段。
前面提到的sealed,表示該類型不能被派生,它就像java的final。它並不僅僅用於類型上,連方法也可以,表示我們不想繼承自該類的類型再覆寫該方法 。
5.類型的設計問題
類型有這么多的關鍵字和可見性,導致我們在設計類的時候可能就會有疑問:到底我該怎樣限定它們的可見性和添加合適的關鍵字呢?這個問題其實我在Java那里就已經有較為基本的認識,這里結合C#重新說一下。
C#編譯器默認生成非密封類,但類應該是密封的。原因有下面幾個方面:
1.版本控制:如果類一開始是密封的,在可預見的未來我們就可以在不破壞兼容性的前提下更改為非密封的,但如果類是非密封的,就沒有機會將它改為密封的(如果這樣做,后果很嚴重,因為它后面的一系列派生類就會全部陣亡)。java中我還真沒有想過這個問題,因為繼承是擴展舊類的方法,雖然我們更喜歡用接口。
2.性能:調用虛方法的性能比不上調用非虛方法,因為CLR必須在運行時判斷對象的類型,而如果是密封類的虛方法,編譯器就會當做非虛方法進行調用,因為不可能會有派生類覆寫該方法。
3.安全性和可預測性:將一個方法,屬性或事件設為virtual,是一件很冒險的事,因為基類會喪失對它的行為和狀態的部分控制權,因為派生類可以選擇覆寫它們。
但密封類缺點也是非常明顯的,就是造成類型的用戶巨大的不方便,因為類功能不會一直不變,類型的使用者會想要為該類型添加新的功能。
所以,這里就有一個折衷的方法:在實現自己的類時,確保將繼承的所有虛方法(包括System.Object中定義的)都密封,同時,不要定義將來可能對版本控制造成負擔的任何方法,比如受保護方法或虛方法。這種方法適合所有OOP語言,包括java。
自定義類型的時候,我們還可以遵循一些原則:
1.除非確定要將該類作為基類使用,並允許派生類對它進行特化(specialization,就是我們添加新的行為或屬性),否則就顯式的指定為sealed,而且采用默認的internal,除非我希望公開該類。就算我認為該類有必要被別人繼承,但不想運行對它進行特化,就可以采取上面那個方法。
2.數據字段,方法,屬性和事件毫無疑問都設為private,體現良好的數據封裝性。幸好C#編譯器默認就是這樣。
OOP的世界中,設計類型是我們最大的工作,如何設計一個復用性高,封裝性好,而且耦合度低的類,是一個艱難的工作,甚至可以說是一門藝術,就像OOP大師那樣。
6.字段
最后我們簡單的講一下字段(field)。
C#的字段並沒有什么特別之處,但有關它的修飾符是值得說一下的,那就是readonly。readonly字段只能在構造器中寫入,也就是對象一旦創建,我們就無法寫入了(可利用反射修改,因為反射可以獲取構造器)。注意,如果是引用類型的字段標記為readonly,不可改變的是引用而不是引用所指的對象,就像java的final引用。