實現繼承

前面文章學習了繼承的一些基本概念,下面就說說繼承的具體實現。

如果要聲明一個類派生於另一個類,可以使用下面的語法:

class MyDerivedClass : MyBaseClass
{
//函數和數據成員等

}

注意:

注意C#不支持私有繼承,因此基類名上沒有public或private限定符。支持私有繼承會大大增加語言的複雜性。

如果類(或結構)也派生於接口,則用逗號分隔開基類和接口:

public class MyDerivedClass : MyBaseClass, IInterface1, IInterface2

{}

對於結構,語法如下

public struct MyDerivedStruct : IInterface1, IInterface2

{}

如果在類定義中沒有指定基類,C#編譯器就假定System.Object是基類。因此下面的兩段代碼生成相同的結果:

class MyClass : Object  {}

class MyClass {}

C#支持object關鍵字,它用作System.Object類的假名,所以也可以編寫下面的代碼

class MyClass : object {}

1、 虛方法

把一個基類函數聲明爲virtual,該函數就可以在派生類中重寫了:

class MyBaseClass
{
public virtual string VirtualMethod()
{
return "";
}
}

也可以把屬性聲明爲virtual。對於虛屬性或重寫屬性,語法與非虛屬性是相同的,但要在定義中加上關鍵字virtual,

public virtual string ForeName
{
get { return foreName; }
set { foreName = value; }
}
private string foreName;

C#中虛函數的概念與標準OOP概念相同:可以在派生類中重寫虛函數。在調用方法時,會調用對象類型的合適方法。在C#中,函數在默認情況下不是虛擬的,但(除了構造函數以外)可以顯式地聲明爲virtual。

class MyDerivedClass : MyBaseClass
{
public override string VirtualMethod()
{
return "override";
}
}

成員字段和靜態函數都不能聲明爲virtual,因爲這個概念只對類中的實例函數成員有意義。

2  隱藏方法

如果簽名相同的方法在基類和派生類中都進行了聲明,但該方法沒有聲明爲virtual 和 override,派生類方法就會隱藏基類方法

   class MyBaseClass
    {
        public string Method()
        {
            return "Base Method!";
        }
    }

   class RealizeClass : MyBaseClass     {       public string Method()         {             return "imi Method";         }     }

在使用基類的新方法編譯代碼時,程序在應該調用哪個方法上就會有潛在的衝突。這在C#中完全合法,但因爲我們的MyGroovyMethod()與基類的MyGroovyMethod()不相關,運行這段代碼的結果就可能不是我們希望的結果。

在C#中,應使用new關鍵字聲明我們要隱藏一個方法
 class RealizeClass : MyBaseClass    {    

      public new string Method()        {            return "imi Method";        }   

 }

3  調用函數的基類版本

C#有一種特殊的語法用於從派生類中調用方法的基類版本:base.<MethodName>()

例如,假定派生類中的一個方法要返回基類的方法返回的值的90%

class CustomerAccount
{
public virtual decimal CalculatePrice()
{
// implementation
return 0.0M;
}
}  
class GoldAccount : CustomerAccount
{
public override decimal CalculatePrice()
{
return base.CalculatePrice() * 0.9M;
}
}

這個語法類似於Java,但Java使用關鍵字super而不是base。

注意,可以使用base.<MethodName>()語法調用基類中的任何方法,不必在同一個方法的重載中調用它。

4  抽象類和抽象函數

C#允許把類和函數聲明爲abstract,抽象類不能實例化,而抽象函數沒有執行代碼,必須在非抽象的派生類中重寫。

抽象函數也是虛擬的(但也不需要提供virtual關鍵字,實際上,如果提供了該關鍵字,就會產生一個語法錯誤)。如果類包含抽象函數,該類將也是抽象的,也必須聲明爲抽象的:

abstract class Building
{
public abstract decimal CalculateHeatingCost(); 

}

5  密封類和密封方法

C#允許把類和方法聲明爲sealed。對於類來說,這表示不能繼承該類;對於方法來說,這表示不能重寫該方法。

sealed class FinalClass
{
}

java中可以把C#中的sealed當作Java中的final。

在把類或方法標記爲sealed時,最可能的情形是:如果要對庫、類或自己編寫的其他類進行操作,則重寫某些功能會導致錯誤。

把方法聲明爲sealed也可以實現類似的目的

class MyClass
{
public sealed override void FinalMethod()
{
 }
}

6  派生類的構造函數

假定沒有爲類定義任何顯式的構造函數,這樣編譯器就會爲所有的類提供默認的構造函數,在後臺會進行許多操作,編譯器可以很好地解決層次結構中的所有問題,每個類中的每個字段都會初始化爲默認值。但在添加了一個我們自己的構造函數後,就要通過派生類的層次結構高效地控制構造過程,因此必須確保構造過程順利進行,不要出現不能按照層次結構進行構造的問題。

abstract class GenericCustomer
{
private string name;
// lots of other methods etc.
}
class Nevermore60Customer : GenericCustomer
{
private uint highCostMinutesUsed;
// other methods etc.
}

使用new運算符實例化Nevermore60Customer時,會發生什麼情況:GenericCustomer customer = new Nevermore60Customer();

成員字段name和highCostMinutesUsed都必須在實例化customer時進行初始化。如果沒有提供自己的構造函數,而是僅依賴默認的構造函數,name就會初始化爲null引用,highCostMinutesUsed初始化爲0。

highCostMinutesUsed字段沒有問題:編譯器提供的默認Nevermore60Customer構造函數會把它初始化爲0。

name呢?顯然,Nevermore60Customer構造函數不能初始化這個值。字段name聲明爲private,這意味着派生的類不能訪問它。默認的Nevermore60Customer構造函數甚至不知道存在這個字段。唯一知道這個字段的是GenericCustomer的其他成員,即如果對name進行初始化,就必須在GenericCustomer的某個構造函數中進行。無論類層次結構有多大,這種情況都會一直延續到最終的基類System.Object上。

1. 在層次結構中添加無參數的構造函數

public abstract class GenericCustomer
{
private string name;
public GenericCustomer()
: base()  // we could omit this line without affecting the compiled code
{
name = "<no name>";
}

如果編譯器沒有在起始花括號的前面找到對另一個構造函數的任何引用,它就會假定我們要調用基類構造函數--這符合默認構造函數的工作方式。

base 和 this關鍵字是調用另一個構造函數時允許使用的唯一關鍵字,其他關鍵字都會產生編譯錯誤。還要注意只能指定一個其他的構造函數。

要通過構造函數的層次結構把級數弄亂的最好方法是把構造函數聲明爲私有:

 private GenericCustomer()
{
name = "<no name>";
}

如果試圖這樣做,就會產生一個有趣的編譯錯誤,如果不理解構造是如何按照層次結構由上而下的順序工作的,這個錯誤會讓人摸不着頭腦.

有趣的是,該錯誤沒有發生在GenericCustomer類中,而是發生在Nevermore60Customer派生類中。編譯器試圖爲Nevermore60Customer生成默認的構造函數,但又做不到,因爲默認的構造函數應調用無參數的GenericCustomer構造函數。把該構造函數聲明爲private,它就不可能訪問派生類了。

如果爲GenericCustomer提供一個帶有參數的構造函數,但沒有提供無參數的構造函數,也會發生類似的錯誤。

2. 在層次結構中添加帶參數的構造函數

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是一個變量,我們需要聲明它,這樣構造函數才能在其主體中處理這個參數。

並不是所有的Nevermore60Customers都有聯繫人,所以還需要有一個不需此參數的構造函數(或爲它提供默認值的構造函數)。實際上,我們指定如果沒有聯繫人,referrerName字段就設置爲<None>。下面是這個帶一個參數的構造函數

 public Nevermore60Customer(string name)
: this(name, "<None>")
{
}

樣就正確建立了所有的構造函數。執行下面的代碼時,檢查事件鏈是很有益的:

GenericCustomer customer = new Nevermore60Customer("Arabel Jones");

編譯器認爲它需要帶一個字符串參數的構造函數,所以它確認的構造函數就是剛纔定義的那個構造函數

  public Nevermore60Customer(string Name)
: this(Name, "<None>")

在實例化customer時,就會調用這個構造函數。之後立即把控制權傳送給對應的Nevermore60Customer構造函數,該構造函數帶2個參數,分別是Arabel Jones和<None>。在這個構造函數中,把控制權依次傳送給GenericCustomer構造函數,該構造函數帶有1個參數,即字符串Arabel Jones。然後這個構造函數把控制權傳送給System.Object默認構造函數。現在執行這些構造函數,首先執行System.Object構造函數,接着執行GenericCustomer構造函數,初始化name字段。然後帶有兩個參數的Nevermore60Customer構造函數得到控制權,把聯繫人的姓名初始化爲<None>。最後,執行Nevermore60Customer構造函數,該構造函數帶有1個參數-- 這個構造函數什麼也不做。

 

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