深入剖析C#的多態

一、什麼是多態  

可以把一組對象放到一個數組中,然後調用它們的方法,在這種場合下,多態性作用就體現出來了,這些對象不必是相同類型的對象。當然,如果它們都繼承自某個類,你可以把這些派生類,都放到一個數組中。如果這些對象都有同名方法,就可以調用每個對象的同名方法。

同一操作作用於不同的對象,可以有不同的解釋,產生不同的執行結果,這就是多態性。多態性通過派生類重載基類中的虛函數型方法來實現。

在面向對象的系統中,多態性是一個非常重要的概念,它允許客戶對一個對象進行操作,由對象來完成一系列的動作,具體實現哪個動作、如何實現由系統負責解釋。

“多態性”一詞最早用於生物學,指同一種族的生物體具有相同的特性。在C#中,多態性的定義是:同一操作作用於不同的類的實例,不同的類將進行不同的解釋,最後產生不同的執行結果。C#支持兩種類型的多態性:

● 編譯時的多態性

編譯時的多態性是通過重載來實現的。對於非虛的成員來說,系統在編譯時,根據傳遞的參數、返回的類型等信息決定實現何種操作。

● 運行時的多態性

運行時的多態性就是指直到系統運行時,才根據實際情況決定實現何種操作。C#中,運行時的多態性通過虛成員實現。

編譯時的多態性爲我們提供了運行速度快的特點,而運行時的多態性則帶來了高度靈活和抽象的特點。

二、實現多態

多態性是類爲方法(這些方法以相同的名稱調用)提供不同實現方式的能力。多態性允許對類的某個方法進行調用而無需考慮該方法所提供的特定實現。例如,可能有名爲 Road 的類,它調用另一個類的 Drive 方法。這另一個類 Car 可能是 SportsCar 或 SmallCar,但二者都提供 Drive 方法。雖然 Drive 方法的實現因類的不同而異,但 Road 類仍可以調用它,並且它提供的結果可由 Road 類使用和解釋。

可以用不同的方式實現組件中的多態性:

● 接口多態性。

● 繼承多態性。

● 通過抽象類實現的多態性。

接口多態性

多個類可實現相同的“接口”,而單個類可以實現一個或多個接口。接口本質上是類需要如何響應的定義。接口描述類需要實現的方法、屬性和事件,以及每個成員需要接收和返回的參數類型,但將這些成員的特定實現留給實現類去完成。

組件編程中的一項強大技術是能夠在一個對象上實現多個接口。每個接口由一小部分緊密聯繫的方法、屬性和事件組成。通過實現接口,組件可以爲要求該接口的任何其他組件提供功能,而無需考慮其中所包含的特定功能。這使後續組件的版本得以包含不同的功能而不會干擾核心功能。其他開發人員最常使用的組件功能自然是組件類本身的成員。然而,包含大量成員的組件使用起來可能比較困難。可以考慮將組件的某些功能分解出來,作爲私下實現的單獨接口。

根據接口來定義功能的另一個好處是,可以通過定義和實現附加接口增量地將功能添加到組件中。優點包括:

1.簡化了設計過程,因爲組件開始時可以很小,具有最小功能;之後,組件繼續提供最小功能,同時不斷插入其他的功能,並通過實際使用那些功能來確定合適的功能。

2.簡化了兼容性的維護,因爲組件的新版本可以在添加新接口的同時繼續提供現有接口。客戶端應用程序的後續版本可以利用這些接口的優點。

通過繼承實現的多態性

多個類可以從單個基類“繼承”。通過繼承,類在基類所在的同一實現中接收基類的所有方法、屬性和事件。這樣,便可根據需要來實現附加成員,而且可以重寫基成員以提供不同的實現。請注意,繼承類也可以實現接口,這兩種技術不是互斥的。

C# 通過繼承提供多態性。對於小規模開發任務而言,這是一個功能強大的機制,但對於大規模系統,通常證明會存在問題。過分強調繼承驅動的多態性一般會導致資源大規模地從編碼轉移到設計,這對於縮短總的開發時間沒有任何幫助。

何時使用繼承驅動的多態性呢?使用繼承首先是爲了向現有基類添加功能。若從經過完全調試的基類框架開始,則程序員的工作效率將大大提高,方法可以增量地添加到基類而不中斷版本。當應用程序設計包含多個相關類,而對於某些通用函數,這些相關類必須共享同樣的實現時,您也可能希望使用繼承。重疊功能可以在基類中實現,應用程序中使用的類可以從該基類中派生。抽象類合併繼承和實現的功能,這在需要二者之一的元素時可能很有用。

通過抽象類實現的多態性

抽象類同時提供繼承和接口的元素。抽象類本身不能實例化,它必須被繼承。該類的部分或全部成員可能未實現,該實現由繼承類提供。已實現的成員仍可被重寫,並且繼承類仍可以實現附加接口或其他功能。

抽象類提供繼承和接口實現的功能。抽象類不能示例化,必須在繼承類中實現。它可以包含已實現的方法和屬性,但也可以包含未實現的過程,這些未實現過程必須在繼承類中實現。這使您得以在類的某些方法中提供不變級功能,同時爲其他過程保持靈活性選項打開。抽象類的另一個好處是:當要求組件的新版本時,可根據需要將附加方法添加到基類,但接口必須保持不變。

何時使用抽象類呢?當需要一組相關組件來包含一組具有相同功能的方法,但同時要求在其他方法實現中具有靈活性時,可以使用抽象類。當預料可能出現版本問題時,抽象類也具有價值,因爲基類比較靈活並易於被修改。

示例:實現多態性的程序

using System ;

public class DrawingBase

{

public virtual void Draw( )

{

Console.WriteLine("I'm just a generic drawing object.") ;

}

}

public class Line : DrawingBase

{

public override void Draw( )

{ Console.WriteLine("I'm a Line.") ; }

}

public class Circle : DrawingBase

{

public override void Draw( )

{ Console.WriteLine("I'm a Circle.") ; }

}

public class Square : DrawingBase

{

public override void Draw( )

{ Console.WriteLine("I'm a Square.") ; }

}

public class DrawDemo

{

public static int Main(string[] args)

{

DrawingBase [] dObj = new DrawingBase [4];

dObj[0] = new Line( ) ;

dObj[1] = new Circle( ) ;

dObj[2] = new Square( ) ;

dObj[3] = new DrawingBase( ) ;

foreach (DrawingBase drawObj in dObj)

drawObj.Draw( ) ;

return 0;

}

}

 

說明:上面程序演示了多態性的實現。在DrawDemo類中的Main( )方法中,創建了一個數組,數組元素是DrawingBase類的對象。該數組名爲dObj,是由四個DrawingBase類型的對象組成。接下來,初始化dObj數組,由於Line,Circle和Square類都是DrawingBase類的派生類,所以這些類可以作爲dObj數組元素的類型。如果C#沒有這種功能,你得爲每個類創建一個數組。繼承的性質可以讓派生對象當作基類成員一樣用,這樣就節省了編程工作量。 一旦數組初始化之後,接着是執行foreach循環,尋找數組中的每個元素。在每次循環中,dObj 數組的每個元素(對象)調用其Draw( )方法。多態性體現在:在運行時,各自調用每個對象的Draw( )方法。儘管dObj 數組中的引用對象類型是DrawingBase,這並不影響派生類重載DrawingBase類的虛方法Draw( )。 在dObj 數組中,通過指向DrawingBase基類的指針來調用派生類中的重載的Draw( )方法。

輸出結果是:

I'm a Line.

I'm a Circle.

I'm a Square.

I'm just a generic drawing object.

在DrawDemo 程序中,調用了每個派生類的重載的Draw( )方法。 最後一行中,執行的是DrawingBase類的虛方法Draw( )。這是因爲運行到最後,數組的第四個元素是DrawingBase類的對象。

三、虛方法

當類中的方法聲明前加上了virtual 修飾符,我們稱之爲虛方法,反之爲非虛。使用了virtual 修飾符後,不允許再有static, abstract, 或override 修飾符。

示例1:帶有虛方法的類

using System ;

public class DrawingBase

{

public virtual void Draw( )

{ Console.WriteLine("這是一個虛方法!") ; }

}

說明:這裏定義了DrawingBase類。這是個可以讓其他對象繼承的基類。該類有一個名爲Draw( )的方法。Draw( )方法帶有一個virtual修飾符,該修飾符表明:該基類的派生類可以重載該方法。DrawingBase類的 Draw( )方法完成如下事情:輸出語句"這是一個虛方法!"到控制檯。

示例2:帶有重載方法的派生類

using System ;

public class Line : DrawingBase

{

public override void Draw( )

{ Console.WriteLine("畫線.") ; }

}

public class Circle : DrawingBase

{

public override void Draw( )

{ Console.WriteLine("畫圓.") ; }

}

public class Square : DrawingBase

{

public override void Draw( )

{ Console.WriteLine("畫正方形.") ; }

}

說明:上面程序定義了三個類。這三個類都派生自DrawingBase類。每個類都有一個同名Draw( )方法,這些Draw( )方法中的每一個都有一個重載修飾符。重載修飾符可讓該方法在運行時重載其基類的虛方法,實現這個功能的條件是:通過基類類型的指針變量來引用該類。

對於非虛的方法,無論被其所在類的實例調用,還是被這個類的派生類的實例調用,方法的執行方式不變。而對於虛方法,它的執行方式可以被派生類改變,這種改變是通過方法的重載來實現的。

下面的例子說明了虛方法與非虛方法的區別。

using System ;

class A

{

public void F( ) { Console.WriteLine("A.F") ; }

public virtual void G( ) { Console.WriteLine("A.G") ; }

}

class B: A

{

new public void F( ) { Console.WriteLine("B.F") ; }

public override void G( ) { Console.WriteLine("B.G") ; }

}

class Test

{

static void Main( )

{

B b = new B( ) ;

A a = b;

a.F( ) ;

b.F( ) ;

a.G( ) ;

b.G( ) ;

}

}

例子中,A 類提供了兩個方法:非虛的F 和虛方法G 。類B 則提供了一個新的非虛的方法F, 從而覆蓋了繼承的F; 類B 同時還重載了繼承的方法G 。那麼輸出應該是:A.F B.F B.G B.G

注意到本例中,方法a.G( ) 實際調用了B.G,而不是A.G,這是因爲編譯時值爲A,但運行時值爲B ,所以B 完成了對方法的實際調用。

在派生類中對虛方法進行重載

先讓我們回顧一下普通的方法重載,普通的方法重載指的是:類中兩個以上的方法(包括隱藏的繼承而來的方法),取的名字相同,只要使用的參數類型或者參數個數不同,編譯器便知道在何種情況下應該調用哪個方法。

而對基類虛方法的重載是函數重載的另一種特殊形式。在派生類中重新定義此虛函數時,要求的是方法名稱,返回值類型、參數表中的參數個數、類型順序都必須與基類中的虛函數完全一致。在派生類中聲明對虛方法的重載,要求在聲明中加上override 關鍵字,而且不能有new, static 或virtual 修飾符。

看一個用汽車類的例子來說明多態性的實現的程序:

using System ;

class Vehicle//定義汽車類

{

public int wheels; //公有成員輪子個數

protected float weight; //保護成員重量

public Vehicle(int w,float g)

{

wheels = w;

weight = g;

}

public virtual void Speak( )

{

Console.WriteLine( " the w vehicle is speaking!" ) ;

}

};

class Car:Vehicle //定義轎車類

{

int passengers; //私有成員乘客數

public Car(int w,float g,int p) : base(w,g)

{

wheels = w;

weight = g;

passengers = p;

}

public override void Speak( )

{

Console.WriteLine( " The car is speaking:Di-di!" ) ;

}

}

class Truck:Vehicle //定義卡車類

{

int passengers; //私有成員乘客數

float load; //私有成員載重量

public Truck (int w,float g,int p, float l) : base(w,g)

{

wheels = w;

weight = g;

passengers = p;

load = l;

}

public override void Speak( )

{

Console.WriteLine( " The truck is speaking:Ba-ba!" ) ;

}

public static void Main( )

{

Vehicle v1 = new Vehicle(0,0 ) ;

Car c1 = new Car(4,2,5) ;

Truck t1 = new Truck(6,5,3,10) ;

v1.Speak( ) ;

v1 = c1;

v1.Speak( ) ;

c1.Speak( ) ;

v1 = t1;

v1.Speak( ) ;

t1.Speak( ) ;

}

}

分析上面的例子我們看到:

● Vehicle 類中的Speak 方法被聲明爲虛方法,那麼在派生類中就可以重新定義此方法。

● 在派生類Car 和Truck 中分別重載了Speak 方法,派生類中的方法原型和基類中的方法原型必須完全一致。

● 在Test 類中,創建了Vehicle 類的實例v1, 並且先後指向Car 類的實例c1 和Truck 類的實例t1。

運行該程序結果應該是:

The Vehicle is speaking!

The car is speaking:Di-di!

The car is speaking:Di-di!

The truck is speaking:Ba-ba!

The truck is speaking:Ba-ba!

這裏,Vehicle 類的實例v1 先後被賦予Car 類的實例c1, 以及Truck 類的實例t1的值。在執行過程中,v1 先後指代不同的類的實例,從而調用不同的版本。這裏v1 的Speak 方法實現了多態性,並且v1.Speak 究竟執行哪個版本,不是在程序編譯時確定的,而是在程序的動態運行時,根據v1 某一時刻的指代類型來確定的,所以還體現了動態的多態性。

四、接口多態性

多個類可實現相同的“接口”,而單個類可以實現一個或多個接口。接口本質上是類需要如何響應的定義。接口描述類需要實現的方法、屬性和事件,以及每個成員需要接收和返回的參數類型,但將這些成員的特定實現留給實現類去完成。

組件編程中的一項強大技術是能夠在一個對象上實現多個接口。每個接口由一小部分緊密聯繫的方法、屬性和事件組成。通過實現接口,組件可以爲要求該接口的任何其他組件提供功能,而無需考慮其中所包含的特定功能。這使後續組件的版本得以包含不同的功能而不會干擾核心功能。

其他開發人員最常使用的組件功能自然是組件類本身的成員。然而,包含大量成員的組件使用起來可能比較困難。可以考慮將組件的某些功能分解出來,作爲私下實現的單獨接口。

根據接口來定義功能的另一個好處是,可以通過定義和實現附加接口增量地將功能添加到組件中。優點包括:

● 簡化了設計過程,因爲組件開始時可以很小,具有最小功能;之後,組件繼續提供最小功能,同時不斷插入其他的功能,並通過實際使用那些功能來確定合適的功能。

● 簡化了兼容性的維護,因爲組件的新版本可以在添加新接口的同時繼續提供現有接口。客戶端應用程序的後續版本可以利用這些接口的優點(如果這樣做有意義)。

五、繼承多態性

多個類可以從單個基類“繼承”。通過繼承,類在基類所在的同一實現中接收基類的所有方法、屬性和事件。這樣,便可根據需要來實現附加成員,而且可以重寫基成員以提供不同的實現。請注意,繼承類也可以實現接口,這兩種技術不是互斥的。

C# 通過繼承提供多態性。對於小規模開發任務而言,這是一個功能強大的機制,但對於大規模系統,通常證明會存在問題。過分強調繼承驅動的多態性一般會導致資源大規模地從編碼轉移到設計,這對於縮短總的開發時間沒有任何幫助。看下面的例子:

class B

{ public virtual void foo () {} }

class D : B

{

public override void foo () {}

}

//試圖重載一個非虛的方法將會導致一個編譯時錯誤,除非對該方法加上“new”關鍵字,//以指明該方法意欲隱藏父類的方法。

class N : D

{

public new void foo () {}

public static void Main() {

N n = new N ();

n.foo( ) ; // 調用N的foo

((D)n).foo( ) ; // 調用D的foo

((B)n).foo( ) ; // 調用D的foo

}

}

和C++、Java相比,C#的override關鍵字使得閱讀源代碼時可以清晰地看出哪些方法是重載的。不過,使用虛方法有利有弊。第一個有利點是:避免使用虛方法輕微的提高了執行速度。第二點是可以清楚地知道哪些方法會被重載。

我們看一個關於飛機描述的類。假設我們有一個描述飛機的基類。現在,我們要完成一個飛機控制系統,有一個全局的函數fly,它負責讓傳遞給它的飛機起飛,那麼,只需要這樣:

using System ;

class plane

{

public virtual void fly(){} //起飛純虛函數

public virtual void land() {} //着陸純虛函數

public virtual string modal(){} //查尋型號純虛函數

}

// 然後,我們從plane派生出兩個子類,直升機(copter)和噴氣式飛機(jet):

class copter:plane

{

private String fModal ;

public override void fly(){}

public override void land(){}

public override string modal(){}

}

class jet : plane

{

private String fModal ;

public override void fly(){}

public override void land(){}

public override string modal{}

}

就可以讓所有傳給它的飛機(plane的子類對象)正常起飛!不管是直升機還是噴氣機,甚至是現在還不存在的,以後會增加的飛碟。因爲,每個子類都已經定義了自己的起飛方式。

可以看到 plane.fly()函數接受參數的是 plane類對象引用,而實際傳遞給它的都是 plane的子類對象,多態性是允許你將父對象設置成爲和一個或更多的他的子對象相等的技術,賦值之後,父對象就可以根據當前賦值給它的子對象的特性以不同的方式運作。 很顯然,parent = child; 就是多態的實質!因爲直升機“是一種”飛機,噴氣機也“是一種”飛機,因此,所有對飛機的操作,都可以對它們操作,此時,飛機類就作爲一種接口。多態的本質就是將子類類型的指針賦值給父類類型的指針(在OP中是引用),只要這樣的賦值發生了,多態也就產生了,因爲實行了“向上映射”。

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