繼承
任何面向對像的程序設計語言都必須提供兩個重要的特性:繼承性(inheritance)和多態性(polymorphism).繼承的引入,就是在類之間建立一种相交關系,使得新定議的派生類的實例可以繼承已有的基類的特征和能力,而且可以加入新的特性或者是修改已有的特性,建立起類的層次.同一操作作用於不同的對像,可以有不同的解釋,產生不同的執行結果,這就是多態性.多態性通過派生類重載基類中的虛函數型方法來實現.
注意:
C#中,派生類只能從一個類中繼承.
C#中,派生類從它的直接基類中繼承成員:方法,域,屬性,事件,索引指示器.
除了構造函數和析構函數,派生類隱式地繼承了直接基類的所有成員.
下面是一個關於車輛的例子
using System;
class Vehicle //定義汽車類
{
int wheels; //公有成員,輪子個數.
protected float weight; //保掮成員,重量.
public Vehicle() {;}
public Vehicle(int w,float g)
{
wheels=w;
weight=g;
}
public 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;
}
}
Vehicle作為基類,體現了"汽車"這個實體有的公共性質;汽車都有輪子和重量,Car類繼承了Vehicle的這些性質,並且添加了自己身的特性:可以搭載乘客.
覆蓋
類的成員聲明中,可以聲明與繼而來的成員同名的成員.這時我們稱派生類的成員覆蓋
(hide)了基類的成員.這种情況下,編譯器不會報告錯誤,但會給出一個警告.對派生類的成
員使用new關鍵字,可以關閉這個警告.
前面的汽車類的例子中,類Car繼承了Vehicle的Speak()方法.我們可以給Car類也聲明一個
Speak()方法,覆蓋Vehicle中的Speak,見下面的代碼.
using System;
class Vehicle //定義汽車類
{
int wheels; //公有成員,輪子個數.
protected float weight; //保掮成員,重量.
public Vehicle() {;}
public Vehicle(int w,float g)
{
wheels=w;
weight=g;
}
public 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;
}
new public void Speak()
{
Console.WriteLine("Di-Di");
}
}
注意:
如果在成員聲明中加上了new關鍵字修飾,而該成員事實上並沒有覆蓋繼承的成員,編譯器將
會給出警告,在一個成員聲明同時使用new和override則編譯器會報告錯誤.
base保留字
base關鍵字主要是為派生類調用基類成員提供一個簡寫的方法.我們先看一個例子程序代碼
class A
{
public void F()
{
//.........
}
public int this[int nIndex]
{
get{};
set{};
}
}
class B:A
{
public void G()
{
int x=base[0];
base.F();
}
}
類B從類A中繼承,B的方法G中調用了A的方法F和索引指示器.方法F在進行編譯時等價於:
public void G()
{
int x=(A (this))[0];
(A (this)).F();
}
使用base關鍵字對基類成員的訪問格式為:
base . indentifier
base [expression-list]
多態性
在面向對像的系統中,多態性是一個非常重要的概念,它允許客戶對一個對像進行操作,由對
像來完成一系列的動作,具體實現哪個動作,如何實現由系統負責解釋.
C#支持兩種類型的多態性
編譯時的多態性
編譯時的多態性是通過重載來實現的.
運行時的多態性
運行時的多態性就是指直到系統運行時,才根據實際情況決定實現何种操作.C#中,運行時的
多態性通過虛成員實現.
編譯時的多態性為我們提供了運行速度快的特點,而運行時的多態多性則帶來了高度靈活和
抽像的特點.
虛方法
當類中的方法聲明前加上了virtual修飾符,我們稱之為虛方法,反之為非虛.使用了virtual
修飾符後,不允許再有static,abstract或override修飾符.
對於非虛的方法,無論被其所在類的實例調用,還是被這個類的派生類的實例調用,方法的執
行方式不變.而對於虛方法,它的執行方式可以被派生類改變,這种改變是通過方法的重載來
實現的.
下面的例子說明了虛方法與非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
B.F
B.G
B.G
例子中,A類提供了兩個方法;非虛的F和虛方法G.類B則提供了一個新的非虛的方法F(),從而
覆蓋了繼承的F:類B同時還重載了繼承的方法G
注意到本例中,方法a.G()實際調用了B.G,而不是A.G.這是因為編譯時值為A,但運行時值為
B,所以B完成了對方法的實際調用.
在派生類中對虛方法進行重載
下面用汽車的例子來說明多態性的實現.
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!");
}
}
class Test
{
public static void Main()
{
Vehicle v1 = new Vehicle(1,2);
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();
}
}
程序運行結果:
the w 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類中的Speak方法被聲明為虛方法,那麼在派生類中就可以重新定義此方法.
在派生類Car和Truck中分別重載了Speak方法,派生類中的方法原型和基類中的方法原型必
須完全一致.
在Test類中,創建了Vehicle類的實例v1,並且先後指向Car類的實例c1和Truck類的實例t1.
這里,Vehicle類的實例v1先後被賦予Car類的實例c1,以及Truck類的實例t1的值.在執行過
程中,v1先後指代不同的類的實例,從而調用不同的版本.這里v1的Speak方法實現了多態性,
並且v1.Speak()究竟執行哪個版本,不是在程序編譯時確定的,而是在程序的動態運行時,根
據v1某一時刻的指代類型來確定的,所以還體現了動態的多態性.
抽像與密封
抽像類
有時候,基類並不與具體的事物相聯系,而是只表達一种抽像的概念,用以為它的派生類提供
了一個公共的界面.為此,C#中引入了抽像類(abstract class)的概念.
抽像類使用abstract修飾符,對抽像類的使用有以下幾點規定:
抽像類只能作為其它類的基類,它不能直接被實例化,而且對抽像類不能使用new操作符.抽
像類如果含有抽像的變量或值,則它們要麼是null類型,要麼包含了對非抽像類的實例的引
用.
抽像類允許包含抽像成員,雖然這不是必須的.
抽像類不能同時又是密封的.
如果一個非抽像類從抽像類中派生,則其必須通過重載來實現所有繼承而來的抽像成員.請
看下面的示例:
abstract class A
{
public abstract void F();
}
abstract class B:A
{
public void G(){}
}
class C:B
{
public override void F(){}
}
抽像類A提供了一個抽像方法F.類B從抽像類A中繼承,並且又提供了一個方法G;因為B中並沒
有包含對F的實現,所以B也必須是抽像類.類C從類B中繼承,類中重載了抽像方法F,並且提供
了對F的具體實現,則類C允許是非抽像的.
讓我們繼續研究汽車類的例子,我們從"交通工具"這個角度來理確Vehicle類的話,它應該表
達一种抽像的概念,我們可以把它定議為抽像類,由轎車類Car和卡車類Truck來繼承這個抽
像類,它們作為可以實例化的類.
using System;
abstract 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.WirteLine("The truck is speaking:Ba-ba!");
}
}
抽像方法
具體的實現交給派生類通過重載來實現.
一個方法聲明中如果加上了abstract修飾符,我們稱該方法為抽像方法(abstract method)
如果一個方法被聲明也是抽像的,那麼該方法默認也是一個虛方法.事實上,抽像方法是一個
新的虛方法,它不提供具體的方法實現代碼.非虛的派生類要求通過重載為繼承的虛方法提
供自己的實現,而抽像方法則不包含具體的實現內容,所以方法聲明的執行體中只有一個分
號";".
只能在抽像類中聲明抽像方法.對抽像方法,不能再使用static或virtual修飾符,而且方法
不能有任何可執行代碼,哪怕只是一對大括號中間加一個一個分號"{;}"都不允許出現,只需
要給出方法的原型就可以了.
"交通工具"的"鳴笛"這個方法實上是沒有什麼意義的,接下來我們利用抽像方法的概念繼續
改寫這個類的例子:
using System;
abstract class Vehicle
{
public int wheels;
protected float weight;
public Vehicle(int w,float g)
{
wheels=w;
weight=g;
}
public abstract void Speak();
}
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.WirteLine("The truck is speaking:Ba-ba!");
}
}
還要注意,抽像方法在派生類中不能使用base關鍵字來進行訪問.例如下面的寫法在編譯時
會發生錯誤:
class A
{
public abstract void F();
}
class B:A
{
public override void F()
{
base.F(); //錯誤,base.F是抽像方法
}
}
我們還可以利用抽像方法來重載基類的虛方法,這時基類中的虛方法的執行代碼就被"撋截"
了.下面的例子說明了這一點:
class A
{
public virtual void F()
{
Console.WriteLine("A.F");
}
}
abstract class B:A
{
public abstract override void F();
}
class C:B
{
public override void F()
{
Console.WriteLine("C.F");
}
}
類A聲明了一個虛方法F,派生類B使用抽像方法重載了F,這樣B的派生類C就可以重載F並提供
自己的實現.
密封類
密封類在聲明中使用sealed修飾符,這樣就可以防止該類被其它類繼承.如果試圖將一個密
封類作為其它類的基類,C#將示出錯.再所當然,密封類不能同時又是抽像類,因為抽像總是
希望被繼承的.
在哪些場合下使用密封類呢?密封類可以阻止其它程序員在無意中繼承該類,而且密封可以
起到運行時優化的效果.實際上,密封類中不可能有派生類,如果密封類實例中存在虛成員函
數可以轉化為非虛的,函數修飾符virtual不再生效,看看下面這個例子.
abstract class A()
{
public abstract void F();
}
sealed class B:A
{
public override void F()
{
/////////////////
}
}
如果我們嘗試寫下面的代碼:
class C:B{}
C#會指出這個錯誤,告訴你B是一個密封類,不能試圖從B中派生任何類.
密封方法
C#還提出了密封方法(sealed method)的概念,以防止在方法所在類的派生類中對該方法的
重載.對方法可以使用sealed修飾符,這時我們稱該方法是一個密封方法.不是類的每個成員
方法都可以作為密封方法,密封方法必須對基類的虛方法進行重載,提供具體的實現方法,所
以在方法的聲明中,sealed修飾符總和override修飾符同時使用.請看下面的例子:
using System;
class A
{
public virtual void F()
{
Console.WriteLine("A.F");
}
public virtual void G()
{
Console.WriteLine("A.G");
}
}
class B:A
{
sealed override public void F()
{
Console.WriteLine("B.F");
}
override public void G()
{
Console.WriteLine("B.G");
}
}
class C:B
{
override public void G()
{
Console.WriteLine("C.G");
}
}
類B對基類A中的兩個虛方法均進行了重載,其中F方法使用了sealed修飾符,成為一個密封方
法.G方法不是密碼方法,所以在B的派生類中,可以重載方法G,但不能重載方法F.
繼承中關於屬性的一些問題
和類的成員方法一樣,我們也可以定議屬性的重載,虛屬性,抽像屬性以及密封屬性的概念.與類和方法一樣,屬性的修飾符也應符合下列規則:
從上面可以看出,屬性的這些規則與方法十分類似,對於屬性的訪問器,我們可以把get訪問
器看成是一個與屬性修飾符相同,沒有參數,返回值為屬性的值類型的方法,把set訪問器看
成是一個與屬性修飾符相同,僅含有一個value慘數,返回類型為void的方法.下面我們用客
戶住宿的例子來說明屬性在繼承中的一些問題.
using System;
public enum sex
{
woman,
man,
};
abstract public class People
{
private string s_name;
public virtual string Name
{
get{return s_name;}
}
private sex m_sex;
public virtual sex Sex
{
get{return m_sex;}
}
protected string s_card;
public abstract string Card
{
get;set;
}
}
上面的例子中聲明了"人"這個類,人的姓名Name和性別Sex是兩個只讀的虛屬性;身份證號
Card是一個抽像屬性,允許讀寫.因為類People中包含了抽像屬性Card,所以People必須聲明
是抽像的.下面我們為住宿的客人編寫一個類,類從Peple中繼承.
class Customer:People
{
string s_no;
int i_day;
public string No
{
get {return s_no;}
set
{
if(s_no!=value)
{
s_no=value;
}
}
}
public int Day
{
get {return i_day;}
set
{
if(i_day!=value)
{
i_day=value;
}
}
}
public override string Name
{
get {return base.Name;}
}
public override sex Sex
{
get {return base.Sex;}
}
public override string Card
{
get {return s_card;}
set {s_card=value;}
}
}
在類Customer中,屬性Name,Sex和Card的聲明都加上了override修飾符,屬性的聲明都與基
類People中保持一致.Name和Sex的get訪問器,Card的get和set訪問器都使用了base關鍵字
來訪問基類People中的訪問器.屬性Card的聲明重載了基類People中抽像訪問器.這樣,在
Customer類中沒有抽像成員的存在,Customer可以是非虛的.