C#編程(二十三)----------實現繼承
原文鏈接:http://blog.csdn.net/shanyongxu/article/details/46593809
如果要聲明派生自另一個類的一個類,可以使用下面的語法:
class DerivedClass: BaseClass
{
//function and data members here
}
這個語法類似於C++和Java中的語法,但是,C++程序員習慣使用公共和私有繼承的概念;註意C#不支持私有繼承,因此在基類名上沒有public或者private限定符.支持私有繼承指揮大大增加語言的復雜性,實際上私有繼承在C++中也很少使用.
如果類(或結構)也派生子接口,則使用逗號分隔列表中的基類和接口.
public class DerivedClass: BaseClass,InterFace1,Interface2
{
}
對於結構:
public struct DerivedClass: Interface1,Interface2
{}
如果在類中沒有定義基類,C#編譯器就假定System.Object是基類.例如:
class MyClass:Object
{}
和
class MyClasst
{}
這兩種方式是相同的結果,第二種方式比較常用,因為比較簡單.C#支持object關鍵字,它用作System.Object類的假名,所以可以這麽寫:
class MyClass : object
{}
如果要引用Object類,就可以使用object關鍵字,VS會識別它.
虛方法
把一個基類函數聲明為virtual,就可以在任何派生類中重寫該函數:
class BaseClass
{
public virtual string VirtualMethod()
{
return “the method is virtual and defined int BaseClass”;
}
}
也可以把屬性聲明為virtual.對於虛屬性或重寫屬性,語法和非虛屬性相同,但是要在定義中天劍virtual關鍵字,語法如下:
public virtual string foreName
{
get{return foreName;}
set{foreName=value;}
}
虛方法的規則同樣適用於虛屬性.可以在派生來中重寫虛函數.在調用方法時,會調用該類對象的合適方法.在C#中,函數在默認情況下不是虛擬的,但是(除了構造函數以外)可以顯示的聲明virtual.這遵循C++的方法,即從性能的角度來看,除非顯式指定,否則函數就不是虛函數.而在JAVA中,所有的函數都是虛擬的.單C#和C++的語法不通,因為C#要求在派生類的函數重寫另一個函數時,要使用override關鍵字現實生命.
例如:
class DerivedClass: BaseClass
{
public override string VirtualMethod()
{
return “this is an override defined in DerivedClass”;
}
}
重寫方法的語法避免了C++中很容易發生的潛在運行錯誤:當派生類的方法簽名無意中與基類版本略有差別時,該方法就不能重寫基類的方法.在C#中,者會出現一個編譯錯誤,因為編譯器會認為函數已標記為override,單沒有重寫其基類的方法.
成員字段和靜態函數都不能生命為virtual,因為這個概念只對類中的函數成員有意義.
例如:
class BaseClass
{
public virtual string fun()
{
return "BaseClass method";
}
}
class DerivedClass : BaseClass
{
public override string fun(string str)
{
return "DerivedClass method";
}
}
隱藏方法
如果簽名相同的方法在基類和派生類中都進行了聲明,但是該方法沒有聲明為virtual和override,派生類方法就會隱藏基類方法.
大多數情況下,是要重寫方法,而不是隱藏方法,因為隱藏方法會造成對於給定類的實例調用錯誤方法的危險.但是,如下例,C#語法可以確保開發人員在編譯時受到這個潛在錯誤的警告,從而使隱藏方法更加安全.
假定有一個類HisBaseClass:
class HisBaseClass
{
//various members
}
在將來的某一刻,要編寫一個派生類,用它給HisbaseClass添加某個功能,特別是要添加該基類中目前沒有的方法----MyMethod():
class MyDerivedClass:HisBaseClass
{
public int MyMethod()
{
//something
return 0;
}
}
一年後,基類的編寫者決定擴展基類的額功能.為了保持一致,他也添加一個名為MyMethod()的方法,該方法的名稱和簽名玉前面添加的方法相同,但是並不完成相同的工作.在使用基類的新方法編譯代碼時,程序在應該調用那個方法上就會有潛在的沖突.這在C#中完全合法,但因為MyMethod()與基類的MyMethod()不相關,運行這段代碼就可能會產生以外的結果.C#可以很好地處理這種沖突.
此時,編譯時系統會發出警告.在C#中,要隱藏一個方法應使用new關鍵字,語法如下:
class MyDerivedClass : HisBaseClass
{
public new int MyMethod()
{
//something
return 0;
}
}
但是新添加的MyMethod()沒有生命為new,所以編譯器會認為他隱藏了基類的方法,但沒有顯式聲明,因此系統會發出一個警告(這也適用於是否把MyMethod()聲明為vritual).如果願意,就可以給新方法重命名.最好這麽做,因為這會避免許多沖突,但是,如果覺得重命名方法不可能(例如,已經針對其他公司把軟件發布為一個庫,所以無法修改方法的名稱),則所有的已有哭護短代碼仍能正常運行,方法是選擇新添加的MyMethod().這是因為訪問這個方法的任何已有代碼必須通過對MyDerivedClass(或進一步派生的類)的引用進行選擇.
已有的代碼不能通過對HisBaseClass類的引用方法這個方法,因為在對HisBaseClass類的早期版本進行編譯時,會產生一個編譯錯誤.這個問題只會發生在將來編寫的客戶端代碼上.C#會發出一個警告,告訴用戶在將來的代碼中可能會出現問題----用戶應該註意這個警告,不要試圖在將來天機的代碼中通過對HisBaseClass的引用調用新的MyMethod()方法,但所有已有的代碼仍會正常工作.這是比較微妙的,但它很好地說明了C#如何處理類的不同版本.
調用函數的基類版本
C#有一種特殊的語法用語從派生類中調用方法的基類版本:base.方法名.例如,假定派生類中的一個方法要返回基類的方法90%的返回值,就可以使用下面的語法:
class CustomerAccount
{
public virtual decimal CalculatePrice()
{
return 0.0m;
}
}
class GoldAccount:CustomerAccount
{
public override decimal CalculatePrice()
{
return base.CalculatePrice()*0.9m;
}
}
註意,可以使用base.方法名語法調用基類中的任何方法,不必從同一個方法的重載中調用它.
抽象類和抽象函數
使用關鍵字abstract.C#允許把類和函數聲明為abstract.抽象類不能實例化,而抽象函數不能直接調用,必須在非抽象的派生類中重寫.顯然,抽象函數本身也是虛函數(盡管不需要提供vritual關鍵字,實際上,如果提供了vritual關鍵字,就會產生一個語法錯誤).如果類包含抽象函數,則該類也是抽象的,必須聲明為抽象的:
abstract class Building
{
public abstract decimal CalculateHeatingCost();//抽象函數
}
C++開發文員還要註意術語上的細微差別:在C++中,抽象函數常常描述為純虛函數,而在C#中,僅使用抽象這個術語.
密封類和密封方法
C#允許把類和方法聲明為sealed.對於類,這表示不能繼承該類;對於方法,表示不能重寫該方法.
sealed class FinaClass
{
}
class DerivedClass:FinaClass//這是錯誤的
{
}
在把類或方法標記為sealed時,最可能的情形是:如果要對庫,類或自己編寫的其他類作用域之外的類或方法進行操作,則重寫某些功能導致代碼混亂.一般情況下,把類或成員標記為sealed是要小心,因為這麽做會嚴重限制他的使用方式.及時認為他不能對繼承自一個類或重寫類的某個成員發揮作用,仍有可能在將來的某個時刻,有人會遇到我們沒有預料到的情況,此時這麽做很有用..NET基類庫大量使用了密封類,是希望從這些類中派生出自己的類的第三方開發人員無法訪問這些類.例如:string就是一個密封類
clas MyClass:MyClassBase
{
public sealed override void FinaMethod()
{}
}
class DerivedClass : MyClass
{
public override void FinaMethod();//錯誤
}
要在方法或屬性上使用sealed關鍵字,必須先從基類上把它聲明為要重寫的方法或者屬性.如果基類不希望有重寫方法或屬性,就不要把它聲明為vritual.
派生類的構造函數
先來看這樣一段代碼:
abstract class GenericCustomer
{
private string name;
}
class Nevermore60Customer:GenericCustomer
{
private uint hishCostMinutesUsed;
}
構造函數的調用順序實現調用System.Object,再按照層次結構由上向下進行,指導到大編譯器要實例化的類為止,還要註意在這個過程中,每個構造函數都初始化它自己的類中的字段.這是它的一般工作方式,再開始添加自己的構造函數時,也應盡可能的遵循這條規則.
註意構造函數的執行順序.總實現調用的正是基類的額構造函數.也就是說,派生類的構造函數可以在執行過程中調用它可以訪問的任何積累方法,屬性和任何其他成員.因為基類已經構造出來了,其字段也初始化了,這也意味著,如果派生類不喜歡初始化基類的方式,但要訪問數據,就可以改變數據的初始值.但是,好的編程方式是讓基類構造函數來處理其字段.
首先來看最簡單的情況,在層次結構中用一個無參數的構造函數來替換默認的構造函數後,看看會出現什麽情況.假定要把每個人的名字初始化為字符串”<no name>”,而不是null引用.可以這樣:
public abstract class GenericCustomer
{
private string name;
public GenericCustomer()
:base()
{
name=”<no name>”;
}
}
添加這段代碼之後,代碼運行正常.Nevermore60Customer仍有自己的默認構造函數,所以上面描述的事件的順序保持不變,但編譯器會使用自定義的GenericCustomer構造函數,而不是生成默認的構造函數,所以那麽字段按照需要總是初始化為”<no name>”.
這次使用的關鍵字是base,而不是this,表示這是基類的構造函數,而不是要調用的當前的構造函數.在base關鍵字後面的圓括號中沒有參數,這非常重要,因為沒有給基類構造函數傳送任何采納數,所以編譯器必須調用無參數的構造函數.其結果是編譯器會插入要調用的System.Object構造函數的代碼,這正好與默認情況相同.
實際上可以省略這行代碼:
public GenericCustomer()
{
name=”<no name>”;
}
base和this關鍵字是調用另一個構造函數時允許使用的唯一關鍵字,其他關鍵字都會產生編譯錯誤.還要註意只能指定唯一一個其他的構造函數.
但是如果這樣:
private GenericCustomer()
{
name=”<no name>”;
}
把構造函數聲明為私有的,就會產生編譯錯誤.有趣的是,該錯誤不是發生在GenericCustomer類中,而是發生在Nevermore60Customer派生類中.原因是:編譯器試圖為Nevermore60Customer生成默認的構造函數,但是又做不到,因為默認的構造函數應調用無參數的GenericCustomer構造函數.把該構造函數聲明為private,他就不鞥呢訪問派生類了.如果為GenericCustomer提供一個帶參數的構造函數,但同時沒有提供一個無參數的構造函數,也會發生類似的錯誤.在本例中,編譯器不能為GenericCustomer生成默認構造函數,所以當編譯器試圖為任何派生類生成默認的構造函數時,會砸死次發現他不能做到這一點,因為沒有無參數的基類構造函數可以調用.解決辦法是為派生類添加自己的構造函數---實際上不需要在這些構造函數中做任何工作,這樣便一起就不會為這些派生類生成任何默認的構造函數了.
在層次結構中添加帶參數的構造函數
首先是帶一個參數的GenericCustomer構造函數,僅在顧客提供其姓名的時候才實例化顧客.
abstract class GenericCustomer
{
private string name;
public GenericCustomer(string name)
{
this.name=name;
}
}
剛才說過,在編譯器試圖為派生類創建默認的構造函數時,會產生一個編譯錯誤,因為編譯器為Nevermore60Customer生成的默認是構造函數會試圖調用一個無參數的GenericCustomer構造函數,但是GenericCustomer中沒有這樣的構造函數.因為,需要為派生類提供一個構造函數,來避免這個錯誤.
class Nevermore60Customer:GenericCustomer
{
private uint highCostMinutesUsed;
public Nevermore60Customer(string name)
:base(name)
{}
}
現在,Nevermore60Customer對象的實例化只有在提供了包含顧客姓名的字符串時才能進行,這正是我們需要的.有趣的是Nevermore60Customer構造函數對這個字符串所做的處理.他本身不能初始化name字段,因為他不能訪問基類中的私有字段,但可以把顧客姓名傳送給基類,以便GenericCustomer構造函數處理.具體方法是,把先執行的基類構造函數指定為顧客姓名當做參數構造函數.除此之外,不需要執行任何操作.
再來看如下代碼:
class Nevermore60Customer:GenericCustomer
{
public Nevermore60Customer(string name,string referrerName)
:base(name)
{
this.referrerName=referrerName;
}
private string referrerName;
private uint highCostMinutesUsed;
}
構造函數將姓名作為參數,把他傳遞給GenericCustomer構造函數進行處理.referrerName是一個需要聲明的變量,這樣構造函數才能在其主題中處理這個參數.
但是並不是所有人都有聯系人(referrerName),所以看下面的代碼:
public Nevermore60Customer(stiring name)
:this(name,”<none>”)
{
}
這樣就正確的建立了所有的構造函數.執行下面的代碼:
GenericCustomer customer=new Nevermore60Customer(“syx”);
比哪一期認為他需要一個字符串參數的構造函數,所以他確認的構造函數是:
public Nevermore60Customer(stiring name)
:this(name,”<none>”)
{
}
在實例化customer時,就會調用這個構造函數.之後立即把控制權傳遞給對應的Nevermore60Customer構造函數,該構造函數有兩個參數,分別是”syx”和”<none>”.在這個構造函數中,把控制權依次傳遞給GenericCustomer構造函數,該構造函數有一個參數,即字符串”syx”.然後這個構造函數會把控制權傳遞給System.Object默認構造函數,現在才能執行這些構造函數,首先執行object的構造函數,接著執行genericCustomer的構造函數,它初始化name字段,然後帶有兩個參數的Nevermore60Customer的構造函數得到控制權,把聯系人(referrerName)的姓名初始化”<none>”.最後,執行Nevermore60Customer構造函數,該構造函數帶有一個參數----這個構造函數什麽也不做.
該過程很合理,也很簡潔.每個構造函數都負責處理變量的初始化,在這個過程中,正確的實例化了類,以備使用.如果再為類編寫自己的構造函數時遵循同樣的規則,就會發現,即使是最復雜的類也可以順利的初始化.
最後總結一下:
1、 當基類中沒有自己編寫構造函數時,派生類默認的調用基類的默認構造函數
Ex:
public class MyBaseClass { }
public class MyDerivedClass : MyBaseClass { public MyDerivedClass() { Console.WriteLine("我是子類無參構造函數"); }
public MyDerivedClass(int i) { Console.WriteLine("我是子類帶一個參數的構造函數"); }
public MyDerivedClass(int i, int j) { Console.WriteLine("我是子類帶二個參數的構造函數"); } } 此時實例化派生類時,調用基類默認構造函數 2、 當基類中編寫構造函數時,派生類沒有指定調用構造哪個構造函數時,會尋找無參的構造函數,如果沒有則報錯,另外無論調用派生類中的哪個構造函數都是尋找無參的那個基類構造函數,而非參數匹配。
Ex:
public class MyBaseClass { public MyBaseClass(int i) { Console.WriteLine("我是基類帶一個參數的構造函數"); } }
public class MyDerivedClass : MyBaseClass { public MyDerivedClass() { Console.WriteLine("我是子類無參構造函數"); }
public MyDerivedClass(int i) { Console.WriteLine("我是子類帶一個參數的構造函數"); }
public MyDerivedClass(int i, int j) { Console.WriteLine("我是子類帶二個參數的構造函數"); } } 此時實例化派生類時則報錯 3、 基類中編寫了構造函數,則派生類中可以指定調用基類的某個構造函數,使用base關鍵字。
Ex
public class MyBaseClass { public MyBaseClass(int i) { Console.WriteLine("我是基類帶一個參數的構造函數"); } }
public class MyDerivedClass : MyBaseClass { public MyDerivedClass() : base(i) { Console.WriteLine("我是子類無參構造函數"); }
public MyDerivedClass(int i) : base(i) { Console.WriteLine("我是子類帶一個參數的構造函數"); }
public MyDerivedClass(int i, int j) : base(i) { Console.WriteLine("我是子類帶二個參數的構造函數"); } } 此時實例化派生類時使用的帶一個參數的構造函數時,則不會報錯,因為他指定了基類的構造函數。 4、 如果基類中的構造函數不含有無參構造函數,那麽派生類中的構造函數必須全部指定調用的基類構造函數,否則出錯
Ex
public class MyBaseClass { public MyBaseClass(int i) { Console.WriteLine("我是基類帶一個參數的構造函數"); } }
public class MyDerivedClass : MyBaseClass { public MyDerivedClass() { Console.WriteLine("我是子類無參構造函數"); }
public MyDerivedClass(int i) : base(i) { Console.WriteLine("我是子類帶一個參數的構造函數"); }
public MyDerivedClass(int i, int j) { Console.WriteLine("我是子類帶二個參數的構造函數"); } 此時編譯將不能通過
C#編程(二十三)----------實現繼承