Symbian OS 中的Class命名約定(M類)
計算機界的民間傳說在談到“mix-ins”的起源時,普遍認為這一概念最早源于Symbolic's Flavors——一個早期的面向對象編程系統。它的設計者顯然從“史蒂夫的冰淇淋客廳”(麻省理工學院的學生們特別喜歡的一個冰淇店)中獲得了靈感,顧客們在那里選擇不同冰淇淋的風味(香草,草莓,巧克力等等)然后再加入一些混合物(堅果, 奶油軟糖, 巧克力碎屑等等)。
當談到多重繼承時,這就意味著在基礎的“風味”類中加入“mix-in”類來擴展它的功能。我相信這就是Symbian的設計者使用mixin這一術語并且把M設為前綴的原因,盡管在Symbian OS中的多重繼承和mixin的使用要比冰淇淋店中要復雜得多。
總體而言,M類是一個抽象接口類。一個具體類(concrete class)通常把基礎的“風味”類(例如CBase或CBase派生類)作為它的***個基類,然后再加入一個或一個以上的M類“mixin”接口來實現接口函數。在Symbian OS中,M類通常定義為回調(callback)接口或監聽器(observer)接口。
一個M類可以由其它M類繼承得來,下面我給出了兩個例子。***個展示了一個實現接口函數的具體類派生至CBase和一個mixin類—— MDomesticAnimal,MdomesticAnimal類派生于MAnimal類但并沒有對它進行實現,實際上,它是MAnimal接口的一個特化(specialization)。
{
public:
virtual void EatL() =0;
};
class MDomesticAnimal : public MAnimal
{
public:
virtual void NameL() =0;
};
class CCat : public CBase,public MDomesticAnimal
{
public:
virtual void EatL(); // 通過MDomesticAnimal繼承自MAnimal
virtual void NameL(); // 繼承自MDomesticAnimal
…………………………
};
第二個示例展示了CBase的派生類和兩個mixin接口,MRadio和MClock。在這種情況下,MClock并不是MRadio的特化,CClockRadio這個具體類分別繼承并實現了這兩個接口。對于M類繼承而言,這種復雜組合的多重繼承是可以實現的。
{
public:
virtual void TuneL() =0;
};
class MClock
{
public:
virtual void CurrentTimeL(TTime& aTime) =0;
};
class CClockRadio : public CBase, public MRadio, public MClock
{
public:
virtual void TuneL();
virtual void CurrentTimeL(TTime& aTime);
…………………………
};
上面展示了多重接口繼承的使用,這也是在Symbian OS中***鼓勵使用的一種多重繼承形式。其它形式的多重繼承將會產生相當復雜的繼承分級,并且標準基類也沒有考慮這方面的設計問題。因而,對兩個 CBase派生類進行多重繼承將會在編譯時引起二義性問題,只能通過虛擬繼承來解決這一問題:
{…………………………};
class CClass2 : public CBase
{…………………………};
class CDerived : public CClass1,public CClass2
{…………………………};
void TestMultipleInheritance()
{
// 無法編譯, CDerived::new 有二義性
// 應明確定義CBase::new繼承自CClass1還是CClass2?
CDerived* derived = new (ELeave) CDerived();
}
讓我們來考慮一下M類的特性,它可能被認為等價于java語言中的接口。和java接口一樣,M類沒有數據成員,所以M類對象不需要進行初始化,因而也就不必為它編寫構造函數。
自然而然的,你也會謹慎的考慮M類是否應該有析構函數(無論是虛的還是非虛的)。M類的析構函數給它的接口實現方式增加了限制條件,使得它必須在 CBase 的派生類中實現。這是因為析構函數就意味著調用delete,因而就要求對象必須基于heap而不能基于stack建立。這就是說實現類一定派生于 CBase,就是因為這一原因,所有的T類和絕大部分的R類都不存在析構函數。
在通過一個指向接口類的指針擁有一個M類對象時,需要提供一些銷毀M類對象的方法。如果你確信能夠保證你的接口類的實施類一定派生于CBase類,那你可以在M類中提供一個虛析構函數。這樣就可以讓M類的所有者通過調用delete來刪除它。
如果不定義虛析構函數,對一個M類指針進行delete操作將會引發USER 42 panic,這是因為:派生于M類的具體類必然也派生于其它類,如CBase或CBase的派生類,mixin指針應在繼承聲明次序中排在C類之后 [1],并且這個M類的指針會轉而指向M類的子對象,而不是指向有效主對象heap單元的起始位置,這個M類的子對象是用于訪問分配到heap單元上的對象的。
可以使用內存管理代碼來釋放heap空間,User::Free(),但如果待清除單元的定位錯誤將會引發panic。這一問題可以通過使用虛析構函數來解決。
但是,定義M類接口是并不是惟一的解決方案,你也可以考慮定義一個派生于CBase的抽象基類來代替M類。因為CBase恰好已經提供了一個虛析
1 具體類定義格式為
而不是
“風味”C類必須在基類列表的***位,以此強調繼承樹的次序。類似這樣的C類對象也可以通過重載CleanupStack::PushL()來壓入清潔棧(參見第3章)
構函數,并且接口類的實施類也僅限于在C類中實現,所以這也是一個理想的辦法。當然,在這一方法中,實施類將僅限于單重繼承(single inheritance),就象在上面所說的那樣,對CBase的多重繼承將會引起二義性和可怕的“菱形”繼承結構。
總體而言,一個mixin接口類不應關心ownership的實現細節。如果調用者有可能通過指向M類接口的指針擁有一個對象,正如上面所說的那樣,你就必須確保提供一個在M類的所有者在不再需要M類時刪除它的方法。但是,這并不限于通過析構函數進行清除。你也可以提供一個純虛Release()函數來代替析構函數,然后類的所有者就可以說“搞定了”——由實現代碼決定使用何種清除方式(例如一個C類,就可以調用“delete this”來完成)。這是一個更靈活的接口——實施類基于stack或heap實現都可以,它可以實現引用計數,專門的清除或其它等等的功能。順便說一句,它實質上并不能調用清除函數Release()或Close(),但它可以幫助你的調用者。首先,它是可識別的并足以讓你推測出它將要干什么。更重要的是,它能夠讓用戶使用CleanupReleasePushL()函數(參見第3章)。
類似于java的接口,一個M類通常僅有純虛函數。但是,也有例外允許使用非純虛函數。比如說:當某個接口的所有繼承類都具有通用的缺省行為時,給這個接口添加共享的實現就可以減少代碼的冗余和維護等令人頭痛的事情。當然,這個缺省的通用實現功能有很多限制,因為mixin類不能有數據成員。所以通常情況下,所有的虛函數都是通過調用mixin接口的純虛函數實現功能的。
【編輯推薦】