假設老師類設計如下: class 老師類 { 屬性:姓名,性別,生日,工資 行為:吃飯,跑步,教學 } 學生類設計如下: class 老師類 { 屬性:姓名,性別,生日,班級 行為:吃飯,跑步,學習 } 我們秉承著,讓最簡潔的代碼,實現最最強大的功能原則,能否讓上述案例中的重覆代碼進行優化呢?我們能 ...
class 老師類 { 屬性:姓名,性別,生日,工資 行為:吃飯,跑步,教學 }
學生類設計如下:
class 老師類 { 屬性:姓名,性別,生日,班級 行為:吃飯,跑步,學習 }
我們秉承著,讓最簡潔的代碼,實現最最強大的功能原則,能否讓上述案例中的重覆代碼進行優化呢?我們能否將學生類與老師類再進行抽象,得到一個人類?這章節學習繼承與多態。
1. 繼承
繼承是面向對象程式設計中最重要的概念之一。繼承允許我們根據一個類來定義另一個類,這使得創建和維護應用程式變得更容易。同時也有利於重用代碼和節省開發時間。
當創建一個類時,程式員不需要完全重新編寫新的數據成員和成員函數,只需要設計一個新的類,繼承了已有的類的成員即可。這個已有的類被稱為的基類,這個新的類被稱為派生類。
繼承的思想實現了 屬於(IS-A) 關係。例如,哺乳動物 屬於(IS-A) 動物,狗 屬於(IS-A) 哺乳動物,因此狗 屬於(IS-A) 動物。
基類和派生類
一個類可以派生自多個類或介面,這意味著它可以從多個基類或介面繼承數據和函數。
C# 中創建派生類的語法如下:
<訪問修飾符> class <基類> { ... } class <派生類> : <基類> { ... }
現在我們將上述學生類,老師類案例使用繼承,進行代碼優化:
// 把公共的屬性與方法提取出來,封裝成父類 public class Person { public string Name { get; set; } public string Sex { get; set; } public DateTime Birthday { get; set; } public void Eat() { .... } public void Run() { } } // 老師類設計如下: public class Teacher : Person // 繼承Person { public int Salary { get; set;} // 教學方法 public void Teach() { ... } } // 學生類設計如下 public class Student : Person { public string ClassName { get; set; } // 學習方法 public void Study() { ... } }
基類的初始化
派生類繼承了基類的成員變數和成員方法。因此父類對象應在子類對象創建之前被創建。您可以在成員初始化列表中進行父類的初始化。
下麵的程式演示了這點:
class Rectangle { // 成員變數 protected double length; protected double width; public Rectangle(double l, double w) { length = l; width = w; } public double GetArea() { return length * width; } public void Display() { Console.WriteLine("長度: {0}", length); Console.WriteLine("寬度: {0}", width); Console.WriteLine("面積: {0}", GetArea()); } }//end class Rectangle class Tabletop : Rectangle { private double cost; public Tabletop(double l, double w) : base(l, w) { } public double GetCost() { double cost; cost = GetArea() * 70; return cost; } public void Display() { base.Display(); Console.WriteLine("成本: {0}", GetCost()); } } class Program { static void Main(string[] args) { Tabletop t = new Tabletop(4.5, 7.5); t.Display(); Console.ReadLine(); } }
當上面的代碼被編譯和執行時,它會產生下列結果:
長度: 4.5 寬度: 7.5 面積: 33.75 成本: 2362.5
註意
-
C# 中 不支持多繼承基類:一個類只能繼承一個直接父類。
-
繼承需要滿足什麼樣的設計規範?
-
子類們相同特征(共性屬性,共性方法)放在父類中定義。
-
子類獨有的的屬性和行為應該定義在子類自己裡面。
-
-
所有的類都是Object類的子類。
-
子類可以繼承父類的屬性和行為(除靜態屬性和靜態方法外),但是子類不能繼承父類的構造器(如果非要繼承,需要額外的寫代碼)。
2. 方法重寫
什麼是重寫?
“重寫”父類方法就是修改它的實現方式或者說在子類中對它進行重新編寫。
為什麼要重寫父類的方法
通常,子類繼承父類的方法,在調用對象繼承方法的時候,調用和執行的是父類的實現。但是,有時候需要對子類中的繼承方法有不同的實現方式。例如,假設動物存在“跑”的方法,從中繼承有狗類和馬類兩個子類,狗與馬的奔跑速度或者動作都不太一樣。
如何重寫
-
重寫父類的方法要用到override關鍵字(具有override關鍵字修飾的方法是對父類中同名方法的新實現)
-
要重寫父類的方法,前提是父類中該要被重寫的方法必須聲明為virtual或者是abstract類型。給父類中要被重寫的方法添加virtual關鍵字表示可以在子類中重寫它的實現。(註意:C#中的方法預設並不是virtual類型的因此要添加virtual關鍵字才能夠被重寫)
-
virtual關鍵字用於將方法定義為支持多態,有virtual關鍵字修飾的方法稱為“虛擬方法”
聲明虛方法
[訪問修飾符] virtual [返回類型] 方法名(參數列表) { //虛擬方法的實現,該方法可以被子類重寫 }
class Employee { public virtual void EmpInfo() { Console.WriteLine("用virtual關鍵字修飾的方法是虛擬方法"); } } class DervEmployee : Employee { public override void EmpInfo() { base.EmpInfo();//base關鍵字將在下麵拓展中提到 Console.WriteLine("該方法重寫base方法"); } } class Program { static void Main(string[] args) { DervEmployee objDervEmployee = new DervEmployee(); objDervEmployee.EmpInfo(); //註意:objDervEmployee派生類的實例是賦給Employee類的objEmployee的引用, // 所以objEmployee引用調用EmpInfo()方法時 還是調用DervEmployee類的方法 Employee objEmployee = objDervEmployee; objEmployee.EmpInfo(); } }
base關鍵字用於從子類中訪問父類成員。即使父類的方法在子類中重寫,仍可以使用base關鍵字調用。
而且,在創建子類實例時,可以使用base關鍵字調用父類的構造函數。使用base關鍵字只能訪問父類的構造函數、實例方法或實例屬性,而不能訪問基類的靜態方法。
隱藏父類方法
如果父類與子類具有相同的方法,然後父方法並沒有加virtual 關鍵字修飾。此時,子類需要用 new 關鍵字進行隱藏父類的方法.
3. 抽象類
使用
public abstract class A { // Class members here. }
抽象類不能實例化。 抽象類的用途是提供一個可供多個派生類共用的通用基類定義。 例如,類庫可以定義一個抽象類,將其用作多個類庫函數的參數,並要求使用該庫的程式員通過創建派生類來提供自己的類實現。
抽象類也可以定義抽象方法。 方法是將關鍵字 abstract
添加到方法的返回類型的前面。 例如:
public abstract class A { public abstract void DoWork(int i); }
抽象方法沒有實現,所以方法定義後面是分號,而不是常規的方法塊。 抽象類的派生類必須實現所有抽象方法。 當抽象類從基類繼承虛方法時,抽象類可以使用抽象方法重寫該虛方法。 例如:
public class D { public virtual void DoWork(int i) { // Original implementation. } } public abstract class E : D { public abstract override void DoWork(int i); } public class F : E { public override void DoWork(int i) { // New implementation. } }
如果將 virtual
方法聲明為 abstract
,則該方法對於從抽象類繼承的所有類而言仍然是虛方法。 繼承抽象方法的類無法訪問方法的原始實現,因此在上一示例中,類 F 上的 DoWork
無法調用類 D 上的 DoWork
。通過這種方式,抽象類可強制派生類向虛擬方法提供新的方法實現。
4. 密封類
使用
public sealed class D { // Class members here. }
密封類不能用作基類。 因此,它也不能是抽象類。 密封類禁止派生。 由於密封類從不用作基類,所以有些運行時優化可以略微提高密封類成員的調用速度。
在對基類的虛成員進行重寫的派生類上,方法、索引器、屬性或事件可以將該成員聲明為密封成員。 在用於以後的派生類時,這將取消成員的虛效果。 方法是在類成員聲明中將 sealed
關鍵字置於 sealed
關鍵字前面。 例如:
public class D : C { public sealed override void DoWork() { } }
5. 介面
介面定義了所有類繼承介面時應遵循的語法合同。介面定義了語法合同 "是什麼" 部分,派生類定義了語法合同 "怎麼做" 部分。
介面定義了屬性、方法和事件,這些都是介面的成員。介面只包含了成員的聲明(c#8.0以外,介面也可以有預設實現)。成員的定義是派生類的責任。介面提供了派生類應遵循的標準結構。
介面使得實現介面的類或結構在形式上保持一致。
抽象類在某種程度上與介面類似,但是,它們大多只是用在當只有少數方法由基類聲明由派生類實現時。
介面本身並不實現任何功能,它只是和聲明實現該介面的對象訂立一個必須實現哪些行為的契約。
抽象類不能直接實例化,但允許派生出具體的,具有實際功能的類。
高內聚,低耦合:儘量依賴於介面,不依賴於實現,正所謂面向介面編程。說白了,就是為瞭解耦。
定義介面
介面使用 interface 關鍵字聲明,它與類的聲明類似。介面聲明預設是 public 的。下麵是一個介面聲明的實例:
interface IMyInterface { void MethodToImplement(); }
以上代碼定義了介面 IMyInterface
。通常介面命令以 I 字母開頭,這個介面只有一個方MethodToImplement()
,沒有參數和返回值,當然我們可以按照需求設置參數和返回值。值得註意的是,該方法並沒有具體的實現。
實現介面
class InterfaceImplementer : IMyInterface { public void MethodToImplement() { Console.WriteLine("MethodToImplement() called."); } } class Program { static void Main() { InterfaceImplementer iImp = new InterfaceImplementer(); iImp.MethodToImplement(); } }
InterfaceImplementer
類實現了 IMyInterface
介面,介面的實現與類的繼承語法格式類似:
class InterfaceImplementer : IMyInterface
繼承介面後,我們需要實現介面的方法 MethodToImplement()
, 方法名必須與介面定義的方法名一致。
介面繼承
以下實例定義了兩個介面 IMyInterface 和 IParentInterface。
如果一個介面繼承其他介面,那麼實現類或結構就需要實現所有介面的成員。
以下實例IMyInterface
繼承了 IParentInterface
介面,因此介面實現類必須實現 MethodToImplement()
和 ParentInterfaceMethod()
方法:
interface IParentInterface { void ParentInterfaceMethod(); } interface IMyInterface : IParentInterface { void MethodToImplement(); } class InterfaceImplementer : IMyInterface { public void MethodToImplement() { Console.WriteLine("MethodToImplement() called."); } public void ParentInterfaceMethod() { Console.WriteLine("ParentInterfaceMethod() called."); } } class Program { static void Main() { InterfaceImplementer iImp = new InterfaceImplementer(); iImp.MethodToImplement(); iImp.ParentInterfaceMethod(); } }
顯式介面實現
如果一個
public interface IControl { void Paint(); } public interface ISurface { void Paint(); } public class SampleClass : IControl, ISurface { // Both ISurface.Paint and IControl.Paint call this method. public void Paint() { Console.WriteLine("Paint method in SampleClass"); } } class Program { static void Main(string[] args) { SampleClass sample = new SampleClass(); IControl control = sample; ISurface surface = sample; // The following lines all call the same method. sample.Paint(); control.Paint(); surface.Paint(); } }
輸入結果如下:
Paint method in SampleClass Paint method in SampleClass Paint method in SampleClass
但你可能不希望為這兩個介面都調用相同的實現。 若要調用不同的實現,根據所使用的介面,可以顯式實現介面成員。 顯式介面實現是一個類成員,只通過指定介面進行調用。 通過在類成員前面加上介面名稱和句點可命名該類成員。 例如:
public class SampleClass : IControl, ISurface { void IControl.Paint() { System.Console.WriteLine("IControl.Paint"); } void ISurface.Paint() { System.Console.WriteLine("ISurface.Paint"); } } class Program { static void Main(string[] args) { SampleClass sample = new SampleClass(); IControl control = sample; ISurface surface = sample; // sample.Paint(); // 此行報錯,因為顯示介面實現的方法不能直接通過類調用 control.Paint(); surface.Paint(); // 顯示介面實現,只能通過介面調用方法 } }
輸入結果:
IControl.Paint ISurface.Paint
從
interface IAnimal { void Roar() { Console.WriteLine("動物在叫"); } } class Tigger : IAnimal { // 因為Roar()已經有預設實現,則可以不必強制實現,如果實現了,則介面預設實現失效 //public void Roar() //{ // Console.WriteLine("老虎在吼"); //} } class Program { static void Main(string[] args) { IAnimal animal = new Tigger(); animal.Roar(); } }
6. 多態
多態是同一個行為具有多個不同表現形式或形態的能力。
多態性意味著有多重形式。在面向對象編程範式中,多態性往往表現為"一個介面,多個功能"。
多態性可以是靜態的或動態的。在靜態多態性中,函數的響應是在編譯時發生的。在動態多態性中,函數的響應是在運行時發生的。
在 C# 中,每個類型都是多態的,因為包括用戶定義類型在內的所有類型都繼承自 Object。
多態就是同一個介面,使用不同的實例而執行不同操作,如圖所示:
現實中,比如我們按下 F1 鍵這個動作:
如果當前在 Flash 界面下彈出的就是 AS 3 的幫助文檔;
如果當前在 Word 下彈出的就是 Word 幫助;
在 Windows 下彈出的就是 Windows 幫助和支持。
同一個事件發生在不同的對象上會產生不同的結果。
靜態多態性
在編譯時,函數和對象的連接機制被稱為早期綁定,也被稱為靜態綁定。C# 提供了兩種技術來實現靜態多態性。分別為:
-
方法重載
-
運算符重載
運算符重載 基本上用不上,本教案中不給予講解。
方法重載
您可以在同一個範圍內對相同的函數名有多個定義。函數的定義必須彼此不同,可以是參數列表中的參數類型不同,也可以是參數個數不同。不能重載只有返回類型不同的函數聲明。
下麵的實例演示了幾個相同的函數 Add(),用於對不同個數參數進行相加處理:
public class MyMath { public int Add(int a, int b, int c) { return a + b + c; } public int Add(int a, int b) { return a + b; } } class Program { static void Main(string[] args) { MyMath dataClass = new MyMath(); int add1 = dataClass.Add(1, 2); int add2 = dataClass.Add(1, 2, 3); Console.WriteLine("add1 :" + add1); Console.WriteLine("add2 :" + add2); } }
下麵的實例演示了幾個相同的函數 print(),用於列印不同的數據類型:
class Printdata { void print(int i) { Console.WriteLine("輸出整型: {0}", i ); } void print(double f) { Console.WriteLine("輸出浮點型: {0}" , f); } void print(string s) { Console.WriteLine("輸出字元串: {0}", s); } } class Program { static void Main(string[] args) { Printdata p = new Printdata(); // 調用 print 來列印整數 p.print(1); // 調用 print 來列印浮點數 p.print(1.23); // 調用 print 來列印字元串 p.print("Hello Runoob"); Console.ReadKey(); } }
當上面的代碼被編譯和執行時,它會產生下列結果:
輸出整型: 1 輸出浮點型: 1.23 輸出字元串: Hello Runoob
動態多態性
動態多態性是通過 抽象類 / 介面 和 虛方法 實現的。
語法:
父類類型 對象名稱 = new 子類構造器; 介面 對象名稱 = new 實現類構造器;
多態中成員訪問特點
-
方法調用:編譯看左邊,運行看右邊。
-
變數調用:編譯看左邊,運行也看左邊。(多態側重行為多態)
多態的前提
-
有繼承/實現關係;有父類引用指向子類對象;有方法重寫。
優勢
-
在多態形式下,右邊對象可以實現解耦合,便於擴展和維護。
Animal a = new Dog(); a.run(); // 後續業務行為隨對象而變,後續代碼無需修改
-
定義方法的時候,使用父類型作為參數,該方法就可以接收這父類的一切子類對象,體現出多態的擴展性與便利。
多態下會產生的一個問題:
-
多態下不能使用子類的獨有功能
以下實例創建了 Shape 基類,並創建派生類 Circle、 Rectangle、Triangle, Shape 類提供一個名為 Draw 的虛擬方法,在每個派生類中重寫該方法以繪製該類的指定形狀。
public class Shape { public int X { get; private set; } public int Y { get; private set; } public int Height { get; set; } public int Width { get; set; } // 虛方法 public virtual void Draw() { Console.WriteLine("執行基類的畫圖任務"); } } class Circle : Shape { public override void Draw() { Console.WriteLine("畫一個圓形"); base.Draw(); } } class Rectangle : Shape { public override void Draw() { Console.WriteLine("畫一個長方形"); base.Draw(); } } class Triangle : Shape { public override void Draw() { Console.WriteLine("畫一個三角形"); base.Draw(); } } class Program { static void Main(string[] args) { // 創建一個 List<Shape> 對象,並向該對象添加 Circle、Triangle 和 Rectangle var shapes = new List<Shape> { new Rectangle(), new Triangle(), new Circle() }; // 使用 foreach 迴圈對該列表的派生類進行迴圈訪問,並對其中的每個 Shape 對象調用 Draw 方法 foreach (var shape in shapes) { shape.Draw(); } Console.WriteLine("按下任意鍵退出。"); Console.ReadKey(); } }
當上面的代碼被編譯和執行時,它會產生下列結果:
畫一個長方形 執行基類的畫圖任務 畫一個三角形 執行基類的畫圖任務 畫一個圓形 執行基類的畫圖任務 按下任意鍵退出。
7. 作業
-
定義一個介面或者抽象類 MyMath(計算器類), 聲明一個方法 Calculator()
-
分別創建五個子類(加、減、乘,除,求餘 五個子類),用於實現MyMath 介面
-
在Main方法 定義兩個變數,並提示從控制台輸入運算符(+,-,*,/,%),實現輸入不同的運算符調用不能的實現子類。
視頻教程:
譽尚學教育_譽尚學教育騰訊課堂官網 (qq.com)
或者:C# 最強入門編程(.Net 學習系列開山巨作)_嗶哩嗶哩_bilibili
海闊平魚躍,天高任我行,給我一片藍天,讓我自由翱翔。