13 創建接口和定義抽象類

從類繼承是很強大的機制,但繼承真正強大之處是能從接口繼承。接口不包含任何代碼或數據;它只規定了從接口繼承的類必須提供哪些方法和屬性。使用接口,方法的名稱/簽名可以和方法的具體實現完全隔絕。

抽象類在許多方面都和接口相似,只是它們可以包含代碼和數據。然而,可以將抽象類的某些方法指定爲虛方法,指示從抽象類繼承的類必須以自己的方式實現這些方法。

 

1.理解接口

接口就相當於一份協議(contract)。類實現了接口後(簽訂了協議後),接口(協議)就能保證類包含了接口所指定的全部方法。

使用接口,可以真正地將“what" (有什麼)和"how" (怎麼做)區分開。接口指定“有什麼”,也就是方法的名稱、返回類型和參數。至於具體“怎麼做”,或者說方法具體如何實現,則不是接口所關心的。接口描述了類提供的功能,但不描述功能如何實現。

 

1.1定義接口

定義接口和定義類相似,只是使用interface而不是class關鍵字。在接口中按照與類和結構一樣的方式聲明方法,只是不允許指定任何訪問修飾符(public, private protected都不可以用)。另外,接口中的方法是沒有實現的,它們只是聲明。實現接口的所有類型都必須提供自己的實現。所以,方法主體被替換成一個分號。下面是-個例子:

interface IComparable
{

    int CompareTo(object obj);

}

注意:接口不含任何數據;不可以向接口添加字段(私有的也不行);

 

1.2實現接口

爲了實現接口,需要聲明類或結構從接口繼承,並實現接口指定的全部方法。這不是真正的“繼承"一雖然語法一 樣, 而且如同本章稍後會講到的那樣,語義有繼承的大量印記。注意,雖然不能從結構派生,但結構是可以實現接口的(從接口“繼承”)。

 

現在我們以陸棲哺乳動物爲例,要求所有陸棲哺乳動物都提供名爲NumberofLegs(腿數)的方法,它返回一個int值,指出一種哺乳動物有幾條腿。

定義接口:

interface ILandBound
{
    int NumberOfLegs();
}

然後可以在Horse(馬)類中實現該接口,具體就是從接口繼承,併爲接口定義的所有方法提供實現(本例只有一個NumberofLegs方法):

class Horse : ILandBound
{

    // …
    public int NumberLegs()
    {

        return 4;   //馬有四條腿

    }
}

實現接口時,必須保證每個方法都完全匹配對應的接口方法,具體遵循以下幾個規則。

(1)方法名和返回類型完全匹配。

(2)所有參數(包括 ref和out關鍵字修飾符)都完全匹配。

(3)用於實現接口的所有 方法都必須具有public 可訪問性。但如果使用顯式接口實現(即實現時附加接口名前綴,稍後會解釋),則不應該爲方法添加訪問修飾符。

接口的定義和實現存在任何差異,類都無法編譯。

下例定義Horse從Mammal繼承,同時實現ILandBound接口:

class Horse : Mammal,ILandBound
{
    …
}

注意:一個接口(InterfaceA)可以從另一個接口(InterfaceB)繼承,這在技術上稱爲接口擴展而不是繼承。

 

1.3通過接口來引用類

和基類變量能引用派生類對象一樣,接口變量也能引用實現了該接口的類的對象。例如,ILandBound變量能引用Horse對象,如下所示:

Horse myHorse = new Horse(...);

ILandBound MyHorse = myHorse; //合法

通過接口來引用對象是一項相當有用的技術。因爲能由此定義方法來獲取不同類型的實參一隻要類型實現了指定的接口。例如,以下FindL andSpeed方法可獲取任何實現了ILandBound接口的實參:

int FindtL andSpeed(ILandBound landBoundMamal)
{

    //…

}

可用is操作符驗證對象是實現了指定接口的一個類的實例。

例如,以下代碼檢查myHorse變量是否實現了ILandBound 接口,如果是就把它賦給一個

ILandBound變量。

if (myHorse is ILandBound)
(

    ILandBound iLandBoundAnimal  = myHorse;

}

1.4使用多個接口

一個類最多隻能有 一個基類,但可以實現數量不限的接口。類必須實現這些接口聲明

的所有方法。

結構或類要實現多個接口,接口要以逗號分隔。如果還要從一個基類繼承,那麼接口要在基類之後列出。例如,假定已定義了一個IGrazable(草食)接口,它包含ChewGrass(咀嚼草)方法,規定所有草食類動物都要實現自己的ChewGrass方法。在這種情況下,可以像下面這樣定義Horse類,它表明Mammal是基類,而ILandBound和IGrazable是Horse要實現的兩個接口。

class Horse : Marmnal, ILandBound, IGrazable
{

    …

}

1.5顯示實現接口

前面的例子都是隱式實現接口,比如之前講的ILandBound接口和Horse類的代碼,Horse類的NumberOfLegs方法的實現中,沒有指明是ILandBound接口的一部分。

interface ILandBound
{
    int NumberOfLegs();
}


class Horse : ILandBound
{

    …
    public int NumberLegs()
    {
        return 4;   //馬有四條腿
    }

}

當Horse類同時實現多個接口時,問題就出現了。例如,假定要實現馬車運輸系統。一次長途旅行可以被分成幾個階段,或者稱爲幾“站" (legs)"。要跟蹤每匹馬拉馬車跑了幾“站”,可以像下面這樣定義接口:

interface Ijourney
{

    int NumberOfLegs();//跑的站(leg)數

}

現在Horse同時實現這兩個接口

class Horse : ILandBound,IJourney
{
    …
    public int NumberLegs()
    {
        return 4;  
    }
}

代碼合法,但到底是馬有4條腿,還是它拉車拉了4站呢?

解決辦法:井區分哪個方法實現的是哪個接口,應該顯式實現接口。

class Horse : ILandBound, IJourney
{

    //...
    int ILandBound.NumberfLegs()
    {
        return 4;
    }

    int Ijourney.Numberflegs()
    {
        return 3;
    }

}

現在可以清楚地定義馬有4條腿,馬拉車拉了3站。

 

除了爲方法名附加接口名前綴,上述語法還有另一個容 易被人忽視的改變:方法沒有用public標記。如果方法是顯式接口實現的一部分, 就不能爲方法指定訪問修飾符。這造成另一個有趣的問題。在代碼中創建一個Horse變量,兩個NumberOfLegs方法都不能通過該變量來調用,因爲它們都不可見。兩個方法對於Horse類來說是私有的。這個設計是合理的。如果方法能通過Horse類訪問,那麼以下代碼會調用哪一個ILandBound 接口的?還是IJourney接口的?

Horse horse = new Horse();
…

int legs = horse.NumberOfLegs();   //該語句無法編譯

那麼,怎麼訪問這些方法呢?答案是通過恰當的接口來引用Horse對象,如下所示:

Horse horse = new Horse();

IJourney journeyHorse = horse;

int legsInJourney = journeytorse .NumberofLegs();

ILandBound landBoundHorse = horse;

int legsOnHorse = landBoundHorse .NumberofLegs();

建議儘量顯式實現接口。

 

1.6接口的限制

(1)不能在接口中定 義任何字段,包括靜態字段。字段本質上是類或結構的實現細節。

(2)不能在接口中定義任何構造器。構造器也是類或結構的實現細節。

(3)不能在接口中定 義任何析構器。析構器包含用於析構(銷燬)對象實例的語句,詳情參見下一篇博客。

(4)不能爲任何方法指定 訪問修飾符。接口所有方法都隱式爲公共方法。

(5)不能在接口中 嵌套任何類型(例如枚舉、結構、類或其他接口)。

(6)雖然一個接口能從另一個接口繼承, 但不允許從結構或類繼承。結構和類含有實

現:如果允許接口從它們繼承,就會繼承實現。

 

2.抽象類

我們知道,Horse和Sheep都是草食(IGrazable)陸棲(ILandBound)哺乳動物(Mammal),像下面這種情況(兩個類明顯有重複):

// Horse和Sheep都是草食動物

class Horse : Mammal, ILandBound, IGrazable // 馬
{
    …
    vold IGrazable .ChewGrass()
    {
        Console.WriteLine("Chevdng grass");
        //用於描述明嚼草的過程的代碼
    }

}



class Sheep : Mammal, ILandBound, IGrazable // 羊
{
    vold IGrazable.ChewGrass()
    {
        Console.WriteLine("Chewdng grass");
        //和馬咀唱草樣的代碼
    }
}

重複的代碼是警告信號,表明應重構代碼以避免重複,並減少維護開銷。一個辦法是將通用的實現放到專門爲此目的而創建的新類中。換言之,要在類的層次結構中插入一個新類。例如:

class GrazingMammal : Marmal, IGrazable // Grazingammal是指草食性貼乳動物
{
    ...
    void IGrazable. ChewGrass()
    {
        //用於表示明嚼草的通用代碼
        Console.WriteLine("Chewing grass");
    }

class Horse : GrazingManmnal, ILandBound
{

    ...
}

class Sheep : GrazingMamnal, ILandBound
{

    …
}

}

這是一個不錯的方案,但仍然有一件事情不太對:可以實際地創建GrazingMammal類(以及Mammal)的實例,這是不合邏輯的。GrazingMammal(草食性哺乳動物)類存在的目的是提供通用的默認實現。它唯- 的作用就是讓一個具體的草食性哺乳動物(例如馬、羊)類從它繼承。GrazingMamnal類是通用功能的抽象,不是能實際存在的實體。

 

爲了明確聲明不允許創建某個類的實例,必須將那個類顯式聲明爲抽象類,這是用

abstract關鍵字實現的。如下所示:

abstract class GrazingMammal : Mammal, IGrazable

{

…

}

試圖實例化-一個GrazingMammal對象,代碼將無法通過編譯。示例如下:

GrazingMammal myGrazingMamnal = new GrazingMammal(…); //非法

 

2.1抽象方法

抽象類可以包含抽象方法。抽象方法原則上與虛方法相似,只是它不含方法主體。派生類必須重寫(override)這種方法。

下例將GrazingMammal類中的DigestGrass(消化草)方法定義成抽象方法:草食動物可以使用相同的代碼來表示咀嚼草的過程,但它們必須提供自己的DigestGrass方法的實現(即使咀嚼草的過程相同,但消化草的方式不同)。

如果一個方法在抽象類中提供默認實現沒有意義,但又需要派生類提供該方法的實現,就適合定義成抽象方法。

abstract class GrazingMamnal : Mamnal, IGrazable
{

    public abstract void DigestGrass();

    …

}

注意:抽象方法不可以私有

 

3.密封類

如果不想一個類作爲基類使用,可以使用C#提供的sealed(密封)關鍵字防止類被用作基類。

例如:

sealed class Horse : GrazingMamal, ILandBound
{

    …

}

任何類試圖將Horse用作基類都會發生編譯時錯誤。密封類中不能聲明任何虛方法,而且抽象類不能密封。

 

3.1密封方法

可用sealed關鍵字聲明非密封類中的一個單獨的方法是密封的,這意味着派生類不能重寫該方法。只有用override 關鍵字聲明的方法才能密封,而且方法要聲明爲sealed override.可像下面這樣理解interface, virtual, override 和sealed等關鍵字:

(1)interface(接口)引入方法的名稱。

(2)virtual(虛)方法是方法的第一一個實現。

(3)override(重寫)方法是方法的另 一個實現。

(4)sealed(密封)是方法的最後- - .個實現。

 

參考書籍:《Visual C#從入門到精通》

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章