一、繼承和多態(tài)
在面向?qū)ο缶幊讨?,繼承(也稱為派生)讓您能夠創(chuàng)建新類,而這些類繼承了父類(也稱為基類)的特征和行為。
繼承讓您能夠?qū)?fù)雜的問題劃分成多個易于處理的部分。這些部分形成了概念層,根據(jù)您看待問題的角度,這些概念層提高了具體化或通用化程度,通過使用“是一個”關(guān)系以自然的方式描述了問題的層次特征。
ps:多重繼承
繼承的基本理念很簡單,但是很多面向?qū)ο缶幊陶Z言允許派生類繼承多個父類,這稱為多重繼承。
多重繼承并沒有改變類之間存在“是一個”關(guān)系的需求,它是一種功能強大的機制,但與強大功能相伴而生的是實現(xiàn)起來復(fù)雜得多。在試圖理清類的派生鏈時,多重繼承也可能導(dǎo)致二義性。因為當(dāng)類有兩個父類時,在它與特定基類之間將存在兩條繼承路徑。
為消除這種二義性,同時考慮到只有多重繼承才是合適解決方案的情形很少,C#只允許單繼承。
通常,具體化(specialization)指的是新類添加了基類沒有的數(shù)據(jù)或行為。但是當(dāng)基類只聲明了行為而沒有實現(xiàn)它時,也可能發(fā)生具體化。在這種情況下,派生類將負責(zé)提供實現(xiàn)。
通過從現(xiàn)有類型派生出新類型,可繼承父類型的特征和行為。繼承還讓派生類可對基類做多方面的修改,具體如下。
- 新添私有數(shù)據(jù)
- 新添行為
- 重新定義現(xiàn)有的行為
在面向?qū)ο缶幊讨?,多態(tài)指的是可像使用一種類型那樣使用另一種類型。通常,這是通過以下兩種方式實現(xiàn)的。
- 一種類型繼承了另一種類型,讓它能夠訪問的操作和方法與父類型相同
- 兩種類型都實現(xiàn)了一個兼容的公有接口,它們支持相同的操作和共有數(shù)據(jù),但是實現(xiàn)可能不同
多態(tài)嚴重依賴于封裝、抽象和繼承的概念。沒有它們,一個類幾乎不可能替換另一個類。
ps:多態(tài)
單詞polymorphism(多態(tài))看似很復(fù)雜,實際上并非如此。多態(tài)是一種常見的自然現(xiàn)象。它由希臘詞 poly(表示很多)和 morphe(表示形狀或形式)組成,從字面上說,指的是很多形狀或形式。
在C#中實現(xiàn)繼承很容易,只需在類聲明中指定要繼承的父類即可。
如上這種繼承也叫實現(xiàn)繼承(implementation inheritance),因為實際上從父類繼承了實現(xiàn)。
ps:設(shè)計類層次結(jié)構(gòu)
繼承沒有提供的功能之一是刪除數(shù)據(jù)或行為。如果發(fā)現(xiàn)需要將行為或數(shù)據(jù)從某個派生類中刪除,很可能是由于你的類層次結(jié)構(gòu)設(shè)計得不正確。
設(shè)計類層次結(jié)構(gòu)并非總是簡單任務(wù),通常需要嘗試多次才能設(shè)計正確。為此,最佳的方法是多花些時間考慮已知對象和以后可能需要的對象之間的關(guān)系。如果類層次結(jié)構(gòu)太淺(繼承關(guān)系不多)或過深(繼承關(guān)系太多),可能需要重新思考這些對象之間的關(guān)系。
請記住,并非類層次結(jié)構(gòu)中的一切都必須相互關(guān)聯(lián)。類層次結(jié)構(gòu)完全可以由多個更小的層次結(jié)構(gòu)組成。
在C#中,將表達式賦給變量時,其類型必須與變量的類型兼容。這意味著如下代碼非法:
Car c = new Car();
Truck t = c;
這是有道理的,因為卡車和轎車是不同的東西。然而,從邏輯上說,卡車和轎車都是四輪車(FourWheelVehicle),而四輪車是車(Vehicle),因此下述代碼合法:
雖然c和v1指向的是同一個Car對象,但是存在一個重要差別。由于c被聲明為一個類型為Car的變量,因此通過它可以訪問Car及其基類FourWheelVehicle和Vehicle定義的成員;然而,v1被聲明為一個類型Vehicle的變量,因此通過它只能訪問Vehicle定義的成員。
ps:向上轉(zhuǎn)型和向下轉(zhuǎn)型
將派生類對象轉(zhuǎn)換為基類對象稱為向上轉(zhuǎn)型(upcasting),而將基類對象轉(zhuǎn)換為派生類對象稱為向下轉(zhuǎn)型(downcasting)。
雖然可以沿類層次結(jié)構(gòu)上移(從派生類到基類),但是不能下移。例如,下面的代碼非法:
Vehicle v1 = new Vehicle();
Car c = v1;
不能隱式地將更通用類對象賦給更具體的類對象。
要這樣做,必須顯式地告訴編譯器,要將基類對象向下轉(zhuǎn)型為派生類對象。就這里而言,可編寫如下代碼:
Vehicle v1 = new Vehicle();
Car c = (Car)v1;
雖然上述代碼合法,但是確實帶來了一個問題。如果編寫了如下代碼,結(jié)果將如何呢?
Vehicle v1 = new Vehicle();
Vehicle v2 = new Truck();
Car c = (Car)v2;
上述代碼合法,編譯時不會導(dǎo)致錯誤,但是運行時會導(dǎo)致InvalidCastException,指出不能將Truck對象轉(zhuǎn)換為Car對象。
為避免這種問題,方法是遵循“信任并核實”原則。這意味著你相信代碼能夠通過編譯并運行,但在執(zhí)行轉(zhuǎn)換前核實基類變量實際上是正確的派生類型。如下演示了各種完成“信任并核實”的方法。
Car c = new Car();
Truck t = new Truck();
Vehicle v1 = c;
Vehicle v2 = t;
if(typeof(Car).IsAssignableFrom(v1.GetType()))
{
c = (Car)v1;
Console.WriteLine(c.GetType());
}
if (v1 is Car)
{
c = (Car)v1;
Console.WriteLine(c.GetType());
}
c = v1 as Car;
if (c != null)
{
Console.WriteLine(c.GetType());
}
第一種方法使用C#底層類型系統(tǒng)判斷能否將v1的類型(v1.GetType()的結(jié)果)賦給類型Car(方法調(diào)用typeof(Car)的結(jié)果),如果答案是肯定的,就將v1顯式地轉(zhuǎn)換為Car??偸强梢詫⑴缮悓ο筚x給基類變量。
第二種方法更簡單些,它詢問類型系統(tǒng):v1是否是Car,如果是,就將v1顯式地轉(zhuǎn)換為Car。
第三種方法最簡單,它指出:如果v1可轉(zhuǎn)換為Car,就執(zhí)行轉(zhuǎn)換并返回結(jié)果;否則,返回null。
ps:構(gòu)造函數(shù)串接和默認構(gòu)造函數(shù)
如果沒有顯式地串接基類的一個構(gòu)造函數(shù),那么編譯器將嘗試串接默認構(gòu)造函數(shù)。
這里的問題是,并非所有類都有公有的默認構(gòu)造函數(shù),因此如果沒有顯式地串接正確的基類構(gòu)造函數(shù),就可能導(dǎo)致編譯錯誤。
如下示例派生類的構(gòu)造函數(shù):
1.1 處理繼承而來的成員
有時候,派生類需要一個名稱相同但行為不完全相同的方法或?qū)傩裕@可使用成員隱藏(member hiding)來實現(xiàn)。要隱藏基類的成員,可在派生類中聲明一個成員,其簽名與要隱藏的基類成員相同。由于成員隱藏是基于簽名的,因此也可使用它來修改成員的返回類型。
ps:成員隱藏
隱藏基類成員可能導(dǎo)致意外(至少是原本不想要)的結(jié)果。雖然有時故意在派生類中隱藏基類成員,但是成員隱藏通常是這樣做的結(jié)果:對基類的修改(這可能是您能夠控制的,也可能是您無法控制的)導(dǎo)致你無意間隱藏了基類成員。
因此,在基類成員被隱藏時,編譯器將發(fā)出警告,讓你知道這一點。如果你確信這正是你想做的,應(yīng)在聲明派生類成員時使用關(guān)鍵字new。可以使用關(guān)鍵字 new 并不意味著在基類成員被隱藏時不用發(fā)出警告,而只是讓成員隱藏變成顯式的。
為讓問題盡可能清楚,C#要求重寫類成員時使用兩個關(guān)鍵字。在基類中,必須在成員聲明中包含關(guān)鍵字virtual,而在派生類中,必須在成員聲明中包含關(guān)鍵字override。
通常,虛成員實現(xiàn)的行為比較簡單—如果它們提供了行為。虛成員主要用于確保在任何情況下,派生類都有該成員,且將執(zhí)行某種微不足道的默認行為。派生類將重寫虛成員,使其行為更具體、更合適。
就像構(gòu)造函數(shù)一樣,可使用關(guān)鍵字 base 來訪問虛成員的基類實現(xiàn)。關(guān)鍵字 base 類似于關(guān)鍵字this,但指的是基類,而不是當(dāng)前類。
不同于成員隱藏,成員重寫有一定的限制,具體如下:
- 重寫成員的聲明不能改變虛成員聲明的訪問級別
- 虛成員和重寫成員都不能聲明為private的
- 虛成員和重寫成員的簽名必須相同
- 重寫成員的聲明中不能包含修飾符new、static和virtual
ps:默認為虛成員
有些面向?qū)ο蟮木幊陶Z言(如 Java)默認將成員設(shè)置為虛擬的,但是C#不這樣。這意味著對于可能要重寫的成員,必須顯式地使用關(guān)鍵字virtual聲明它,因為只有被聲明為虛擬的成員才能被重寫。
要禁止類成員被重寫或禁止類被繼承,可將其密封。要密封類成員,可使用關(guān)鍵字sealed和override;要密封類,只需使用關(guān)鍵字sealed。
ps:密封類
要正確地設(shè)計類的可繼承性,可能需要做大量的工作。在這方面,有3種選擇:
- 保留類為非密封的,但確保它能被安全地繼承。如果沒有任何人繼承這個類,那么這些工作完全沒有必要
- 保留類為非密封的,但什么也不做。這要求適用房(consumer)明白如何安全地擴展你的類
- 將類密封。如果你確信不會有人繼承你的類,這可能是最好的選擇。以后總是可以對類解除密封,這不應(yīng)對使它的代碼帶來巨大影響。通過將類密封、還使得可以在JIT編譯階段進行額外的運行階段優(yōu)化
二、抽象類和抽象成員
雖然繼承類有很多優(yōu)點,但是有時候需要給派生類提供標準實現(xiàn)并要確保派生類提供特定類或方法的實現(xiàn),或者要求類不能有實例。C#提供了修飾符abstract,可將其用于類和類成員。
ps:靜態(tài)類
編譯器實際上將靜態(tài)類實現(xiàn)為密封的抽象類,以防對其進行實例化或繼承它。
通過將類聲明為抽象的,可禁止對其進行實例化。因此,抽象類的構(gòu)造函數(shù)通常是protected的,而不是public的。如果沒有提供默認構(gòu)造函數(shù),編譯器將創(chuàng)建一個受保護(protected)的默認構(gòu)造函數(shù)。抽象類可包含虛成員、非虛成員和抽象成員。抽象成員是使用修飾符abstract聲明的,且沒有提供實現(xiàn)。
如下所示的Vehicle類是一個抽象類,它包含一個名為Operate的抽象方法。
public abstract class Vehicle
{
int wheels;
protected Vehicle(){}
public Vehicle(int wheels)
{
this.wheels = wheels;
}
public abstract void Operate();
}
在派生類中,可以選擇性地重寫虛成員。與此不同的是,在具體(非抽象)的派生類中,必須重寫抽象成員。如果派生類也是抽象的,就無需重寫基類的抽象成員。在派生類對其進行重寫前,抽象成員沒有實現(xiàn),因此首次重寫成員時,不能調(diào)用基類的該成員。
三、接口
C#不允許繼承多個基類,因此需要精心挑選基類。所幸的是,C#提供了另一種支持多重繼承的方法:接口以公有屬性和方法的方式定義了一組通用的特征和行為,所有派生類都必須實現(xiàn)它們??蓪⒔涌谝暈榉植款愋投x或類型描述。
與抽象類一樣,接口也不能直接實例化,也可以包含方法、屬性和事件。接口不能包含字段、構(gòu)造函數(shù)和析構(gòu)函數(shù)。
ps:接口不是協(xié)定
人們通常說接口定義了任何派生類都必須實現(xiàn)的協(xié)定(contract),這種說法僅在某種意義上說是正確的,即接口定義了派生類可用方法簽名和屬性。
這并沒有指定具體的實現(xiàn),完全可以在滿足接口要求的情況下提供毫無用處的實現(xiàn)。接口只是定義了繼承它的類都有的屬性和方法。
接口的聲明方式與類的聲明方式極其相似,但是需要使用關(guān)鍵字 interface 替換關(guān)鍵字class。接口可以是internal、public、protected或private的,但是如果沒有顯式地指定訪問級別,那么接口的訪問級別將默認為internal。所有接口成員都自動為public的,不能給它們指定訪問修飾符。由于接口成員也自動為抽象的,因此不能給它們提供實現(xiàn)。
當(dāng)類繼承接口時,稱為接口繼承或接口實現(xiàn),它只繼承成員的名稱和簽名,因為接口沒有提供實現(xiàn)。這意味著對于接口定義的所有成員,派生類都必須提供實現(xiàn)。要繼承接口,只需像繼承類那樣指定要繼承的接口的名稱。可繼承多個接口,為此只需將接口名用逗號分隔。
ps:同時繼承基類和接口
C#在繼承列表中使用位置表示法指定基類和接口。如果類同時繼承了一個基類和一個或多個接口,總是首先列出基類。
還可以進行混合繼承,即可以只繼承一個基類、只繼承一個或多個接口、同時繼承一個基類和一個或多個接口。如果基類繼承了接口,其派生類都將繼承該接口的實現(xiàn)。
雖然接口不能繼承類,但是可繼承其他接口。如果將接口視為松散的協(xié)定,那么接口繼承意味著要遵循該協(xié)定,還必須遵循其他協(xié)定。這讓您能夠創(chuàng)建高度具體的接口,然后將這些接口聚合成更大的接口。在有多個不相關(guān)的類需要實現(xiàn)類似功能,如將數(shù)據(jù)存儲到壓縮的ZIP 文件中時,這很有用。在這種情況下,可在接口中定義與這種功能相關(guān)的行為和特征,然后將這些接口作為定義業(yè)務(wù)對象的其他接口的一部分。
ps:接口和擴展方法
對接口來說,擴展方法很有用,因為也可以擴展接口。擴展接口后,實現(xiàn)該接口的所有類型都將獲得相應(yīng)的擴展方法。事實上,用于泛型集合的整個語言集成查詢(Language Integrated Query,LINQ)就是以這種方式實現(xiàn)的。
接口繼承另一個接口時,派生接口只定義新增的成員,而不重新定義被繼承的接口的成員。當(dāng)類實現(xiàn)該聚合接口時,它必須提供所有相關(guān)接口定義的成員。
與抽象類結(jié)合使用時,接口的威力和靈活性將發(fā)揮得淋漓盡致。由于接口的方法隱式為抽象的,因此繼承接口的抽象類無需為接口成員提供實現(xiàn)。相反,它可使用自己的抽象成員替換接口成員,在這種情況下,派生類必須重寫該成員并為其提供實現(xiàn)。
如果類實現(xiàn)了多個接口,而這些接口定義了簽名相同的成員,代碼如下:
interface IVehicle
{
void Operate();
}
interface IEquipment
{
void Operate();
}
class PoliceCar : IVehicle, IEquipment
{
public void Operate()
{
}
}
在這種情況下,編譯器將讓公有方法與兩個接口成員都匹配。如果這不是你想要的,就必須使用顯式接口實現(xiàn)(explicit interface implementation),如下圖所示,這要求你對要實現(xiàn)的成員名進行全限定。在顯式接口實現(xiàn)中,不需要指定訪問修飾符,因為成員隱式為公有的,但必須通過接口顯式地指定要實現(xiàn)的成員。
interface IVehicle
{
void Operate();
}
interface IEquipment
{
void Operate();
}
class PoliceCar : IVehicle, IEquipment
{
void IVehicle.Operate()
{
}
void IVehicle.IEquipment()
{
}
}
這意味著派生類必須通過接口訪問其成員,因此顯式接口實現(xiàn)相當(dāng)于隱藏了成員,以免不小心錯誤地使用它。