類和繼承
類繼承
通過繼承我們可以定義一個新類,新類納入一個已經聲明的類並進行擴展。
- 可以使用已存在的類作為新類的基礎。已存在類稱為基類(base class),新類稱為派生類(derived class)。派生類組成如下:
- 本身聲明中的成員
- 基類的成員
- 聲明派生類,需要在類名后加入基類規格說明
- 派生類擴展它的基類,因為它包含了基類的成員,加上它本身聲明中的新增功能
- 派生類不能刪除它所繼承的任何成員
例:OtherClass類,繼承自SomeClass
class OtherClass:SomeCLass { ↑ ↑ ... 冒號 基類 }
訪問繼承的成員
繼承的成員可以被訪問,就像它們是派生類自己聲明的一樣。
例:下面代碼聲明了類SomeClass和OtherClass
- Main創建派生類OtherClass的一個對象
- Main中接下來的兩行調用基類的Method1,先是使用基類的Field1,然后是派生類的Field2
- Main中后續兩行調用派生類中的Method2,再次先使用基類Field1,然后是派生類的Field2
class SomeClass { public string Field1="base class field "; public void Method1(string value) { Console.WriteLine("Base class -- Method1: {0}",value); } } class OtherClass:SomeClass { public string Field2="derived class field"; public void Method2(string value) { Console.WriteLine("Derived class -- Method2: {0}",value); } } class Program { static void Main() { var oc=new OtherClass(); oc.Method1(oc.Field1); //以基類字段為參數的基類方法 oc.Method1(oc.Field2); //以派生字段為參數的基類方法 oc.Method2(oc.Field1); //以基類字段為參數的派生方法 oc.Method2(oc.Field2); //以派生字段為參數的派生方法 } }
所有類都派生自object類
沒有基類規格說明的類隱式直接派生自類object。
關於類繼承的其他重要內容如下
- 一個類聲明的基類規格說明中只能有一個單獨的類。即單繼承
- 雖然類只能直接繼承一個基類,但繼承的層次沒有限制。
基類和派生類是相對的術語。所有類都是派生類,要么派生自object,要么派生自其他類。
屏蔽基類的成員
雖然派生類不能刪除它繼承的任何成員,但可以用與基類同名的成員來屏蔽(mask)基類成員。這是繼承的主要功能之一,非常實用。
- 要屏蔽一個繼承的數據成員,需要聲明一個新的同類型成員,並使用相同名稱
- 通過在派生類中聲明新的帶有相同簽名的函數成員,可以隱藏或屏蔽繼承的函數成員。(請記住,簽名由名稱和參數列表組成,不包括返回類型)
- 要讓編譯器知道你在故意屏蔽繼承的成員,使用new修飾符。否則,程序可以成功編譯,但編譯器會警告你隱藏了一個繼承的成員
- 也可屏蔽靜態成員
class SomeClass { public string Field1; ... } class OtherClass:SomeClass { new public string Field1; ... }
例:OtherClass派生自SomeClass,但隱藏了兩個繼承的成員。
class SomeClass { public string Field1="SomeClass Field1 "; public void Method1(string value) { Console.WriteLine("SomeClass.Method1: {0}",value); } } class OtherClass:SomeClass { new public string Field1="OtherClass Field1";//屏蔽基類成員 new public void Method1(string value)//屏蔽基類成員 { Console.WriteLine("OtherCLass.Method1: {0}",value); } } class Program { static void Main() { var oc=new OtherClass(); oc.Method1(oc.Field1); } }
基類訪問
如果派生類必須完全的訪問被隱藏的繼承成員,可以使用基類訪問(base access)
Console.WriteLie("{0}",base.Field1);
例:派生類OtherClass隱藏了基類的Field1,但可以使用基類訪問表達式訪問它
class SomeClass { public string Field1="Field1 -- In the base class"; } class OtherClass:SomeClass { new public string Field1="Field1 -- In the derived class"; public void PrintField1() { Console.WriteLine(Field1); Console.WriteLine(base.Field1); } } class Program { static void Main() { var oc=new OtherClass(); oc.PrintField1(); } }
如果你的程序代碼經常使用base進行基類訪問,你可能需要重新評估類的設計。一般來說能有更優雅的設計,但在沒有其他方法的時候也可使用這個特性。
使用基類的引用
如果有一個派生類對象的引用,就可以獲取該對象基類部分的引用。
例:使用基類引用
- 第一行聲明並初始化了變量derived,它包含一個MyDerivedClass類型對象的引用
- 第二行聲明了一個基類類型MyBaseClass的變量,並把derived中的引用轉換為該類型,給出對象的基類部分的引用
- 基類部分的引用被存儲在變量mybc中,在賦值運算符的左邊
- 其他部分的引用不能“看到”派生類對象的其余部分,因為它通過基類類型的引用“看”這個對象
MyDerivedClass derived=new MyDerivedClass(); MyBaseClass mybc=(MyBaseClass)derived;
例:兩個類的聲明和使用
class MyBaseClass { public void Print() { Console.WriteLine("This is the base class."); } } class MyDerivedClass:MyBaseClass { new public void Print() { Console.WriteLine("This is the derived class"); } } class Program { static void Main() { var derived=new MyDerivedClass(); var mybc=(MyBaseClass)derived; derived.Print(); mybc.Print(); } }
虛方法和覆寫方法
虛方法可以使基類的引用訪問“升至”派生類內。
可以使用基類引用調用派生類的方法,只需滿足下面的條件。
- 派生類的方法和基類的方法有相同的簽名和返回類型
- 基類的方法使用virtual標注
- 派生類的方法使用override標注
例:virtual、override演示
class MyBaseClass { virtual public void Print() ... } class MyDerivedClass:MyBaseCLass { override public void Print() ... }
下圖闡明了這組virtual和override方法。注意和上一種情況(用new隱藏基類成員)相比在行為上的區別
- 當使用基類引用(mybc)調用Print方法時,方法調用被傳遞到派生類執行,因為:
- 基類的方法被標記為virtual
- 在派生類中有匹配的override方法
- 下圖闡明了這一點,顯示了一個從virtual Print方法后面開始,並指向override Print方法的箭頭
下面的代碼和上一節相同,但由於使用了virtual和override,產生的結果大不相同。
class MyBaseClass { virtual public void Print() { Console.WriteLine("This is the base class."); } } class MyDerivedClass:MyBaseClass { override public void Print() { Console.WriteLine("This is the derived class"); } } class Program { static void Main() { var derived=new MyDerivedClass(); var mybc=(MyBaseClass)derived; derived.Print(); mybc.Print(); } }
其他關於virtual和override的重要信息
- 覆寫和被覆寫的方法必須有相同的可訪問性。換句話說,當被覆寫為private時,覆寫方法不能是public等
- 不能覆寫static方法和非虛方法
- 方法、屬性和索引器,以及另一種成員類型事件(將在后面闡述),都可以被聲明為virtual和override
覆寫標記為override的方法
覆寫方法可以在繼承的任何層次出現。
- 當使用對象基類部分的引用調用一個覆寫的方法時,方法的調用被沿派生層次上溯執行,一直到標記為override的方法的最高派生(most-derived)版本
- 如果在更高的派生級別有該方法的其他聲明,但沒有被標記為override,那么它們不會被調用
下面來看看,在MyBaseClass中Print標記為virtual,在MyDerivedClass中Pring標記為override。在SecondDerived中,使用override或new聲明Print,兩種情況的區別。
class MyBaseClass { virtual public void Print() { Console.WriteLine("This is the base class."); } } class MyDerivedClass:MyBaseClass { override public void Print() { Console.WriteLine("This is the derived class"); } } class SecondDerived:MyDerivedClass { ... }
情況1:使用override聲明Print
如果把SecondDerived的Print方法聲明為override,那么它會覆寫方法的全部兩個低派生級別版本。如下圖所示,如果一個基類的引用被用於調用Print,它會向上傳遞通過整個鏈達到類SecondDerived中的實現。
下面的代碼實現了這種情況。注意Main方法最后兩行代碼。
- 第一條使用最高派生類SecondDerived的引用調用Print方法。這不是通過基類部分的引用的調用,所以它將調用SecondDerived中實現的方法
- 第二條語句使用基類MyBaseClass的引用調用Print方法
class SecondDerived:MyDerivedClass { override public void Print() { Console.WriteLine("This is the second derived class"); } } class Program { static void Main() { var derived=new SecondDerived(); mybc=(MyBaseClass)derived; derived.Print(); mybc.Print(); } }
情況2:使用new聲明Print
使用new時,當方法通過SecondDerived的引用調用時,結果與情況1相同。但當方法通過MyBaseClass的引用調用時,方法調用只向上傳遞了一級,在MyDerived那就被執行。
class SecondDerived:MyDerivedClass { new public void Print() { Console.WriteLine("This is the second derived class"); } } class Program { static void Main() { var derived=new SecondDerived(); mybc=(MyBaseClass)derived; derived.Print(); mybc.Print(); } }
覆蓋其他成員類型
前面幾節詳述了如何在方法上使用virtual/override。其實在屬性和索引器上也一樣。
例:屬性的virtual/override
class MyBaseClass { private int _myInt=5; virtual public int MyProperty { get{return _myInt;} } } class MyDerivedClass:MyBaseClass { private int _myInt=10; override public int MyProperty { get{return _myInt;} } } class Program { static void Main() { var derived=new MyDerivedClass(); var mybc=(MyBaseClass)derived; Console.WriteLine(derived.MyProperty); Console.WriteLine(mybc.MyProperty); } }
構造函數的執行
- 要創建對象的基類部分,需要隱式調用基類的某個構造函數作為創建實例過程的一部分
- 繼承層次鏈中的每個類在執行它自己的構造函數體之前執行它的基類構造函數
創建一個實例過程中的第一件事是初始化對象的所有實例成員。然后調用基類的構造函數,最后執行該類自己的構造函數。
警告 在構造函數中調用虛方法是極不推薦的。在執行基類的構造函數時,基類的虛方法會調用派生類的覆寫方法,但這是在執行派生類的構造函數方法體之前。因此,調用會在派生類沒有完成初始化之前傳遞到派生類。
構造函數初始化語句
默認情況下,構造對象時,將調用基類的無參數構造函數。但構造函數可以重載,所以基類可能有一個以上的構造函數。如果希望派生類使用一個指定的基類構造函數而不是無參數構造函數,必須在構造函數初始化語句中指定它。
- 使用關鍵字base並指明使用哪一個基類構造函數
- 使用關鍵字this並指明應該使用當前類的哪一個構造函數
例:構造函數示例
- 構造函數初始化語句指明要使用有兩個參數的基類構造函數。且參數類型分別為string、int
- 在基類參數列表中的參數必須在類型和順序方面與已定的基類構造函數的參數列表相匹配
public MyDerivedClass(int x,string s):base(s,x { ... }
當聲明一個不帶構造函數初始化語句的構造函數時,它實際上是帶有base()構造函數初始化語句的簡寫形式,如下圖。
另外一種形式的構造函數初始化語句可以讓構造過程(實際上是編譯器)使用當前類中其他的構造函數。
例:使用同一個類中具有兩個參數的構造函數
public MyClass(int x):this(x,"Using Default String") { ... }
這種語法也常用於另一種情況:一個類有好幾個構造函數,並且它們都需要在對象構造開始時執行一些公共的代碼。對於這種情況,可以把公共代碼提取出來作為一個構造函數,被其他所有的構造函數作為構造函數初始化語句使用,減少代碼冗余。
類訪問修飾符
類的可訪問性有兩個級別:public和internal
- 標記為public的類可以被系統內任何程序集中的代碼訪問
- 標記為internal的類只能被它自己所在的程序集內的類看到
- internal是默認的可訪問級別
下圖闡明了internal和public類從程序集外部的可訪問性。類MyClass對左邊程序集內的類不可見,因為MyClass被標記為internal。然而,類OtherClass對於左邊的類可見,因為它是public。
程序集間的繼承
迄今為止,我們一直在同一程序集內聲明基類和派生類。但C#也允許從不同程序集定義基類和派生類。
要從不同程序集定義基類、派生類,必須具備以下條件。
- 基類必須聲明為public,這樣才能從它的程序集外部訪問
- 必須在Visual Studio工程中的References節點添加對包含該基類的程序集的引用
例:程序集間繼承示例
第一段代碼創建了含有MyBaseClass類的程序集,該類有以下特征
- 它聲明在名稱為Assembly1.cs的源文件中,並位於BaseClassNS命名空間內部
- 它聲明為public,以便從其他程序集訪問它
- 它含有一個單獨成員,一個名為PrintMe的方法
//源文件名稱為Assembly1.cs using System; namespace BaseClassNS { public class MyBaseClass { public void PrintMe() { Console.WriteLine("I am MyBaseClass"); } } }
第二段代碼含有DerivedClass類的聲明,該源文件名稱為Assembly2.cs
- DerivedClass的類體為空,但從MyBaseClass集成了方法PrintMe
- Main創建了一個類型為DerivedClass的對象並調用它繼承的PrintMe方法
//源文件名稱為Assembly2.cs using System; using BaseClassNS; ↑ 包含基類聲明的命名空間 namespace UsesBaseClass { class DerivedClass:MyBaseClass {} class Program { static void Main() { var mdc=new DerivedClass(); mdc.PrintMe(); } } }
成員訪問修飾符
對類的可訪問性,只有兩種修飾符:internal、public。本節闡述成員的可訪問性。
類的可訪問性描述了類的可見性;成員的可訪問性描述了類成員的可見性。
在講解成員訪問性的細節前,先闡述一些通用內容.
- 所有顯式聲明在類聲明中的成員都是相互可見的,無論它們的訪問性如何
- 繼承的成員不在類的聲明中顯式聲明,所以,如你所見,繼承的成員對派生類的成員可以是可見的,也可以是不可見的
- 有以下5個成員訪問級別
- public
- private
- protected
- internal
- protected internal
- 必須對每個成員指定訪問級別。如果不指定,它默認的隱式訪問級別為private
- 成員不能比它的類有更高的可訪問性。
訪問成員的區域
public class MyClass { public int Member1; private int Member2; protected int Member3; internal int Member4; protected internal int Member5; ... }
另一個類(如類B)能否訪問MyClass類中的成員取決於兩個特征
- 類B是否派生自MyClass類
- 類B是否和MyClass在同一程序集
上面兩個特征划分出4個集合。
- 在同一程序集且繼承MyClass
- 在同一程序集但不繼承MyClass
- 在不同程序集且繼承MyClass
- 在不同程序集但不繼承MyClass
這些特征用於定義5種訪問級別,下面詳細介紹。
public的可訪問性
public訪問級別限制性最少。所有類,包括程序集內部的類和外部的類都可以自由地訪問成員。
private的可訪問性
私有成員訪問級別限制最嚴格
- private類成員只能被它自己的類成員訪問。它不能被其他的類訪問,包括繼承它的類
- 然而,private成員能被嵌套在它的類中的類成員訪問(嵌套類在第25章闡述)
protected的可訪問性
protected訪問級別如同private訪問級別,除了一點,它允許派生自該類的類訪問該成員。(注意,即使程序集外部繼承該類的類也能訪問該成員)
internal的可訪問性
internal成員對程序集內部的所有類可見,但對程序集外部的類不可見。
protected internal的可訪問性
protected internal的成員對所有繼承該類的類以及所有程序集內部的類可見。
注意,這是protected和internal的並集。
成員訪問修飾符小結
抽象成員
抽象成員指設計為被覆寫的函數成員。抽象成員有以下特征
- 必須是一個函數成員,即字段和常量不能是抽象成員
- 必須用abstract修飾
- 不能有實現代碼塊。代碼用分號表示
- 共有4個類型的成員可以聲明為抽象:方法、屬性、事件、索引
例:抽象方法和抽象屬性
abstract public void PrintStuff(string s); abstract public int MyProperty { get; set; }
關於抽象成員的注意事項:
- 盡管抽象成員必須在派生類中被覆寫,但不能把virtual和abstract合用
- 類似虛成員,派生類中抽象成員的實現必須指定override修飾符
抽象類
抽象類指設計為被繼承的類。抽象類只能被用作其他類的基類。
- 不能創建抽象類的實例
- 抽象類使用abstract標識
- 抽象類可以包含抽象成員和普通的非抽象成員。
- 抽象類可以派生自另一個抽象類。
- 任何派生自抽象類的類必須使用override關鍵字實現該類所有的抽象成員,除非派生類自己也是抽象類
抽象類和抽象方法示例
abstract class AbClass { public void IdentifyBase() { Console.WriteLine("I am AbClass"); } abstract public void IndetifyDerived(); } class DerivedClass:AbClass { override public void IdentifyDerived() { Console.WriteLine("I am DerivedClass"; } } class Program { static void Main() { // AbClass a=new AbClass();//錯誤,抽象類不能實例化 var b=new DerivedClass(); b.IdentifyBase(); b.IdentifyDerived(); } }
抽象類的另一個例子
例:包含數據成員和函類型成員的抽象類
abstract class MyBase { public int SideLength=10; const int TriangleSideCount=3; abstract public void PrintStuff(string s); abstract public int MyInt{get;set;} public int PerimeterLength() { return TriangleSideCount*SideLength; } } class MyCLass:MyBase { public override void PrintStuff(string s) { Console.WriteLine(s); } private int _myInt; public override int MyInt { get{return _myINt;} set{_myInt=value;} } } class Program { static void Main() { var mc=new MyClass(); mc.PrintStuff("This is a string."); mc.MyInt=28; Console.WriteLine(mc.MyInt); Console.WriteLine("Perimeter Length:{0}",mc.PerimeterLength()); } }
密封類
抽象類必須用作基類,它不能被實例化。
密封類與抽象類相反。
- 密封類只能被用作獨立的類,不能用作基類(被繼承)
- 密封類使用sealed修飾符標注
靜態類
靜態類中所有成員都是靜態的。靜態類用於存放不受實例數據影響的數據和函數。靜態類常見用途就是創建一個包含一組數學方法和值的數學庫。
- 靜態類本身必須標記為static
- 類的所有成員必須是靜態的
- 類可以用一個靜態構造函數,但不能有實例構造函數,不能創建該類的實例
- 靜態類是隱式密封的,即不能繼承靜態類
可以使用類名和成員名,像訪問其他靜態成員那樣訪問它的成員。
例:靜態類示例
static public class MyMath { public static float PI=3.14f; public static bool IsOdd(intx) { return x%2==1; } public static int Times2(int x) { return 2*x; } } class Program { static void Main() { int val=3; Console.WriteLine("{0} is odd is {1}",val,MyMath.IsOdd(val)); Console.WriteLine("{0} * 2 = {1}",val,MyMath.Time2(val)); } }
擴展方法
擴展方法允許編寫的方法和聲明它的類之外的類關聯。
擴展方法的重要要求如下:
- 聲明擴展方法的類必須聲明為static
- 擴展方法本身必須聲明為static
- 擴展方法必須包含關鍵字this作為它的第一個參數類型,並在后面跟着它所擴展的類的名稱
例:擴展方法示例
namespace ExtensionMethods { sealed class MyData { private double D1,D2,D3; public MyData(double d1,double d2,double d3) { D1=d1;D2=d2;D3=d3; } public double Sum() { return D1+D2+D3; } } static class ExtendMyData { public static double Average(this MyData md) { return md.Sum()/3; } } class Program { static void Main() { var md=new MyData(3,4,5); Console.WriteLine("Sum: {0}",md.Sum()); Console.WriteLine("Average: {0}",md.Average()); } } }