C#中的面向物件概念
原著:Tushar Kant Agrawal 12/24/2003
原文:http://www.c-sharpcorner.com/Code/2003/Dec/OOPS In CSharp 1.0.asp
翻譯:lover_P
出處:http://www.cstc.net.cn/docs/docs.php?id=254
在這篇文章中我們將要討論一些面向物件在C#中的具體實踐的關鍵概念。我們將要討論一下面向物件
的基礎包括介面(Interface)、訪問修飾符(Access Modifier)、繼承(Inheritance)、多型(Polymorphism)
等等。
面向物件的關鍵概念
抽象(Abstraction)
封裝(Encapsulation)
多型(Polymorphism)
繼承(Inheritance)
[內容]
? 抽象
o 物件引用
o 訪問修飾符
o 物件的成分
? 介面
? 實現或行為
? 成員或例項變數
? 繼承
o 訪問關鍵字
o 呼叫基類
o 關於繼承的更多提示
o 抽象類
o 介面和抽象類之間的區別
o 重寫概述
o 隱藏基類成員
o 版本
o 封閉類
? 多型性
o 實現多型性
? 總結
抽象
抽象是一種將一個物件歸納為一個數據型別的能力,這個資料型別具有特定的一組特徵(Characteristic)
和能夠執行的一組行為(Action)。
面向物件的(Object-oriented)語言通過類(Class)來提供抽象。類為一個物件(Object)的型別定義了
屬性(Porperty)和方法(Method)。
例如:
? 你可以為狗狗建立一個抽象,它具有一些特徵,如顏色、身高[譯註:狗狗也要記錄身高?]和重
量;還有一些行為,如跑和咬。我們稱這些特徵為屬性,稱這些行為為方法。
? 一個記錄集(Recoredset)物件是對一組資料的集合的抽象表示。
類是物件的藍圖。
物件是類的例項(Instance)。
C#
public class Draw {
// 類程式碼
}
物件引用
當我們在操作一個物件時,我們需要使用對該物件的一個引用(Reference)。相反,當我們操作簡單數
據型別如整型時,我們操作的是實際的值而不是引用。
當我們使用new 關鍵字建立一個新的物件時,我們便將該物件的一個引用存貯在了一個變數中。看下面
的例子:
Draw MyDraw = new Draw();
這段程式碼建立了Draw 類的一個新的例項。我們通過MyDraw 變數來獲得對這個新物件的訪問。這個變
量儲存了對這個物件的引用。
現在我們還有另外一個變數,它具有對同一個物件的引用。我們可以隨意使用這兩個變數,因為他們都
引用了同一個物件。我們需要記得的是我們的這個變數並不是物件本身,它只是對物件本身的一個引用或
指標。
早繫結(Early binding)是指我們的程式碼通過直接呼叫物件的方法來直接與物件進行互動。由於編譯器
事先知道物件的資料型別,它可以直接編譯出調用物件方法的程式碼。早繫結還允許IDE 通過使用智慧感知
(IntelliSense)來幫助我們進行開發工作;它還能夠使編譯器確保我們所呼叫的方法確實存在,以及我們
確實提供了恰當的引數值。
遲繫結(Late Binding)是指我們的程式碼在執行時動態地與物件進行互動。這提供了很大的靈活性,因為
我們的程式碼不必知道它所互動的物件的具體型別,只要這個物件提供了我們需要呼叫的方法就可以了。由
於IDE 和編譯器無法知道物件的具體型別,也就無法進行智慧感知和編譯期間語法檢查;但是,相較而言,
我們卻得到了空前的靈活性。
如果我們通過在我們的程式碼模組的頂部指定“Option Strict On”[譯註:指的是Visual Basic .NET]來
開啟嚴格型別檢查,則IDE 和編譯器將強制進行早繫結。預設的情況下,嚴格型別檢查被設定為關閉狀態,
所以我們可以在我們的程式碼中使用遲繫結。
訪問修飾符
訪問修飾符是一組關鍵字,用於指定在一個型別中宣告的成員的可訪問性。
公有(Public)成員對於任何人都是可見的。我們可以在類內部的和類的子孫的程式碼中通過使用類的實
例來訪問一個公有的成員。
私有(Private)成員是隱藏的,只有對類本身是可用的。所有使用了一個類的例項的程式碼都不能直接訪
問一個私有成員,這個類的子類也不允許。
受保護(Protected)成員和私有成員類似,只能由包含它的類訪問。然而,受保護成員可以由一個類的
子類所使用。如果類中的一個成員可能被該類的子類訪問,它應該宣告未受保護的。
內部/友元(Internal/Friend)成員對整個應用程式是公有的,但對於其他的外部應用程式是私有的。當我
們希望其他應用程式能夠使用當前應用程式中的包含的一個類,但又希望這個類為當前應用程式保留一定
的功能時,就要用到內部/友元成員。在C#
Friend。
受保護內部(Protected Internal)成員只能由包含了基類的應用程式中的從該基類派生的子類所訪問。當
你希望一個類中的成員只能由其子類訪問,並且拒絕其他應用程式訪問該類的這個成員的時候,你就要將
其宣告未受保護內部成員。
物件的成分
我們使用介面來獲得對物件的資料和行為的訪問。物件的資料和行為包含在物件內部,因此,一個客戶
應用程式可以將物件視為黑盒,只有通過它的接口才能獲得可訪問性。這是面向物件的關鍵概念,稱為封
裝。這意味著任何使用這個物件的應用程式不能直接訪問它的行為和資料——必須使用物件的介面。
物件有三個主要部分:
1. 介面(Interface)
2. 實現(Implementation)或行為(Behavior)
3. 成員(Member)或例項變數(Instance variable)
介面
介面定義了一組方法(Method,子程式或函式例程[譯註:指Visual Basic .NET 中的Sub 和Function])、
屬性(Property)、事件(Event)和域(Field,變數或特性),這些都被宣告為公有。
實現或行為
一個方法之內的程式碼稱為實現。有的時候由於這些程式碼可以使物件作一些有用的工作,而稱之為行為。
儘管我們改變了實現,客戶程式仍然可以使用我們的物件——只要我們沒有改變介面。只要我們的方法
的名字和它的引數列表以及返回值型別沒有改變,我們可以隨意地改變它的實現。
因此方法簽名(Method Signature)取決於:
? 方法名稱
? 引數的資料型別
? 引數是按值傳遞還是按引用傳遞[譯註:Visual Basic .NET 中的ByVal 或ByRef]
? [譯註:還有引數的出現順序]
很重要的需要我們緊記的是,封裝只是一個語法工具——它允許我們現有的程式碼可以無需修改而繼續運
行。然而,這不是語義的——這意味著儘管我們現有的程式碼可以執行,但它不一定繼續作我們希望它做的
工作。
成員或例項變數
一個物件的第三個關鍵部分是資料(Data),或狀態(State)。一個類的每一個例項都具有絕對相同的
介面和實現——唯一可以不同的就是特定物件中所包含的資料。
成員變數正是為此而宣告,它對於我們的類總的所有的程式碼都是可用的。典型的成員變數通常被宣告為
私有的——只有對我們的類本身的程式碼有效。它們有時也被稱作例項變數或特性。.NET Framework 也稱它
們為域。
我們不要將例項變數與屬性相混淆。屬性是一種特殊的方法,用來獲取或設定資料;而例項變數是類中
的一個變數,用來儲存被屬性所暴露的資料。
介面看起來像一個類,但沒有實現。其中只包含事件、索引器(Indexer)、方法和/或屬性的定義。介面
中只提供定義的原因是因為它們必須被類和結構繼承,這些類和結構必須對介面中定義的每個成員提供實
現。那麼,沒有實現任何功能的介面有什麼好處呢?他們可以裝配出一個“即插即用”的架構,其中的所有
元件可以隨意替換!由於所有可替換的元件都實現了相同的介面,因此可以不用任何擴充套件程式就可以使用
它們。介面強制每個元件暴露用於特定途徑的公有成員。
因為介面必須有繼承的類和結構所定義,因此它們定義了一個契約。對於例項,如果類foo 是從介面
IDisposable 繼承而來的,這將形成一個條款,確保它具有Dispose()方法,這是IDisposable 介面
中唯一的一個成員。所有希望使用foo 類的程式碼都會檢查foo 類是否繼承自IDisposable 介面。如果
答案為真,程式碼就知道了它可以呼叫foo.Dispose()。
定義一個介面:MyInterface.cs
interface ImyInterface {
void MethodToImplement();
}
上面的程式碼定義了一個名為IMyInterface 的介面。一個通用的命名約定是在所有的介面名字前面添
加字首字母“I”,但這不是強制的。這個介面有一個單獨的名為MethodToImplement()的方法。介面可
以擁有任何型別的、帶有不同引數和不同返回值型別 的方法。注意這個方法沒有實現(花括號“{}”之間的
指令),而是直接以分號“;”結束。這是因為介面僅指定方法的簽名而必須由繼承它的類或結構去實現。
介面中的所有方法都預設為公有的,而且對於任何方法或介面[譯註:指的是介面中宣告的巢狀介面]不
允許出現訪問修飾符(如private、public)。
使用一個介面:InterfaceImplementer.cs
class InterfaceImplementer : IMyInterface {
public void MethodToImplement() {
Console.WriteLine(“MethodToImplement() called.”);
}
}
上面程式碼中的InterfaceImplement 類實現了IMyInterface 介面。指定一個類繼承了一個介面和
指定它繼承了一個類是一樣的,都用到了下面的語法:
class InterfaceImplementer : IMyInterface
注意,既然這個類繼承了IMyInterface 介面,它就必須實現所有的成員。當實現介面中的方法時,
所有這些方法都必須且僅能宣告為公有的。這個類通過實現MethodToImplement()方法來完成這些。
注意這個方法和介面中定義的方法具有完全相同的簽名、引數和方法名稱。任何的不一致都會產生編譯錯
誤。介面同樣還可以繼承自其它介面。下面的程式碼顯示瞭如何實現繼承的介面。
介面繼承:InterfaceInheritance.cs
using System;
interface IParentInterface {
void ParentInterfaceMethod();
}
interface IMyInterface : IParentInterface {
void MythodToImplement();
}
class InterfaceIplementer : IMyInterface {
public void MethodToImplement() {
Console.WriteLine(“MethodToImplement() called.”);
}
public void ParentInterfaceMethod() {
Console.WriteLine(“ParentInterfaceMethod() called.”);
}
}
上面的程式碼中有兩個介面:IMyInterface 介面和它所繼承的IParentInterface 介面。當一個接
口繼承了另一個,實現它的類或結構必須實現整個介面繼承鏈中所有的介面成員。由於上面程式碼中的
InterfaceImplementer 類繼承了IMyInterface,它同時也繼承了IParentInterface。因此,
InterfaceImplementer 類必須實現IMyInterface 介面中指定的MethodToImplement()方法和
IParentInterface 介面中指定的ParentInterfaceMethod()方法。
總的來說,你可以實現一個介面並在類中使用它。介面也可以被另一個介面繼承。任何繼承了一個介面
的類或結構都必須實現介面繼承鏈中所有介面所定義的成員。
繼承
繼承是指一個類——稱為子類[譯註:亦稱派生類],可以基於另一個類——稱為基類[譯註:亦稱父類、
超類]。繼承提供了一種建立物件層次的機制。
繼承使得你能夠在你自己的類中使用另外一個類的介面和程式碼。
標準的基類既可以是第一次宣告的,也可以是[從其它類]繼承的。派生類(Derived class)可以繼承基類
中具有受保護或更高訪問性的成員。除了父類所提供的功能之外,派生類還提供了更多專門的功能。在派
生類中繼承基類成員不是強制的。
訪問關鍵字
base->訪問基類的成員。
this->引用呼叫一個方法的當前物件。
base 關鍵字用於在一個派生類中訪問基類的成員:在基類上呼叫一個以被其它方法重寫了的方法、在
建立派生類的一個例項的時候指定哪一個基類構造器應該被呼叫。對基類的訪問只允許出現在構造器、實
例方法或例項屬性訪問器中。
在下面的例子中,基類Person 和派生類Employee 都有一個名為Getinfo()的方法。通過使用base
關鍵字,可以從派生類中呼叫基類的Getinfo()方法。
// 訪問基類成員
using System;
public class Person {
protected string ssn = “444-55-6666”;
protected string name = “John L. Malgraine”;
public virtual void GetInfo() {
Console.WriteLine(“Name: {0}”, name);
Console.WriteLine(“SSN: {0}”, ssn);
}
}
class Employee : Person {
public string id = “ABC567EFG”;
public override void GetInfo() {
// 呼叫基類中的GetInfo()方法:
base.GetInfo();
Console.WriteLine(“Employee ID: {0}”, id);
}
}
class TestClass {
public static void Main() {
Employee E = new Employee();
E.GetInfo();
}
}
輸出為:
Name: John L. Malgraine
SSN: 444-55-6666
Employee ID: ABC567EFG
基類構造器也可以在派生類中呼叫。要呼叫一個基類構造器,可以使用base()引用基類構造器。當需
要恰當地初始化一個積累的時候這就非常必要了。
下面的例子展示了一個帶有address 引數的派生類構造器:
abstract public class Contact {
private string address;
public Contact(string b_address) {
this.address = b_address;
}
}
public class Customer : Contact {
public Customer(string c_address) : base(c_address) {
}
}
而在這段程式碼中,Customer 類並沒有address,因此它通過在其宣告新增一個冒號和帶有引數的base
關鍵字將引數傳遞給它的基類構造器。這會呼叫Contact 的帶有address 引數的構造器,並將Contact
的address 域初始化。
下面是另外一個例子,也展示了當建立派生類例項時是如何呼叫基類構造器的:
using System;
public class MyBase {
int num;
public MyBase() {
Console.WriteLine(“In MyBase()”);
}
public MyBase(int i) {
num = i;
Console.WriteLine(“in MyBase(int i)”);
}
}
public class MyDerived : MyBase {
static int i = 32;
// 該構造器將呼叫MyBase.MyBase()
public MyDerived(int ii) : base() {
}
// 該構造器將呼叫MyBase.MyBase(int i)
public MyDerived() : base(i) {
}
public static void Main() {
MyDerived md = new MyDerived(); // 呼叫public MyDerived() : base(i)
// 並將i = 32 傳遞給基類
MyDerived md1 = new MyDerived(1); // 呼叫public MyDerived() : base()
}
}
輸出為:
in MyBase(int i)
in MyBase()
下面的例子不會通過編譯。它詳細地說明了一個類定義如果不包括預設構造器後果:
abstract public class Contact {
private string address;
public Contact(string address) {
this.address = address;
}
}
public class Customer : Contact {
public Customer(string address) {
}
}
在這個例子中,Customer 的構造器沒有呼叫基類構造器。這很明顯是個Bug,因為address 域從未
被初始化。
當一個類沒有一個顯式的構造器時,系統會為它指派一個預設構造器。預設構造器自動地呼叫一個預設
的或無參的基類構造器。下面的例子是上述例子中將會出現的一個自動生成的預設構造器:
public Customer() : Contact() {
}
當一個類沒有宣告任何構造器時,這個例子中的程式碼會自動地生成。當沒有定義派生類構造器時,預設
的基類構造器會被隱式地呼叫。一旦定義了一個派生類構造器,無論其是否帶有引數,都不會自動定義上
例中出現的預設構造器。
呼叫基類成員
如果基類的某些成員具有受保護或更高的可訪問性,則派生類可以訪問這些成員。這隻需簡單地在恰當
的上下文環境中使用成員的名字,就好像這個成員是派生類自己的一樣。下面是一個例子:
abstract public class Contact {
private string address;
private string city;
private string state:
private string zip:
public string FullAddress() {
string fullAddress = address + ‘/n’ + city + ‘,’ + state + ‘ ’ + zip;
return fullAddress;
}
}
public class Customer : Contact {
public string GenerateReport() {
string fullAddress = FullAddress();
// 其它操作
return fullAddress;
}
}
在上面的例子中,Customer 類的GenerateReport()方法呼叫了其基類Contact 中的
FullAddress()方法。所有類對其自身的成員都具有完全的訪問,而無須限定詞。限定詞(Qualification)
由用圓點分開的類名字和其成員名字組成——如MyObject.SomeMethod()。這個例子說明派生類可以
和訪問其自身成員一樣訪問基類成員。
關於繼承的更多提示
不能將一個靜態成員標記為重寫(override)、虛擬(virtual)或抽象(abstract)的。因此,下面的一行
語句是錯誤的:
public static virtual void GetSSN()
你不能在派生類中使用base 關鍵字來呼叫父類的靜態方法。
如果上面的例子中我們聲明瞭下面靜態方法:
public class Person {
protected string ssn = “444-55-6666”;
protected string name = “John L. Malgraine”;
public static void GetInfo() {
// 實現
}
}
現在你就不能使用base.GetInfo()來呼叫這個方法了,而必須用Person.GetInfo()來呼叫。在
靜態成員中我們只能訪問靜態域、靜態方法等。
下面的例子會出錯,因為在GetInfo()中我們不能訪問name,因為name 是非靜態的。
pulic class Person {
protected string ssn = “444-55-6666”;
protected string name = “John L. Malgraine”;
public static void GetInfo() {
Console.WriteLine(“Name: {0}”, name);
Console.WriteLine(“SSN: {0}”, ssn);
}
}
虛擬的或抽象的成員不能是私有的。
如果你沒有在派生類中重寫基類中的虛擬方法,你就不能在派生類中使用base 關鍵字來呼叫基類方法。
同時如果你建立了派生類的例項,你只能呼叫派生類的方法;如果你要訪問基類中的方法,只有建立基類
的例項。
當你在派生了中重寫基類中的方法時,你不能降低方法的訪問級別;反之卻可以。也就是說在派生類中
你可以將基類中的受保護方法標記為公有的[譯註:在.NET Framework 1.1 和C#編譯器版本7.10.3052.4 中
是不允許改變重寫方法的訪問修飾符的]。
“this”關鍵字代表:
呼叫一個方法的當前例項。靜態成員函式中沒有this 指標。this 關鍵字只能用於構造器、例項方法
和例項屬性訪問器中。
下面是this 的一般用法:
? 用來限定具有相同名字的成員,例如:
public Employee(string name, string alias) {
this.name = name;
this.alias = alias;
}
上面的例子中,this.name 代表類中的私有變數name。如果我們寫name = name,這表示的是構造
器的引數name 而不是類中的私有變數name。這種情況下私有變數name 是不會被初始化的。
? 用來將該物件傳遞給其它方法,例如:
CalcTax(this);
? 用於索引器,例如:
public int this[int param] {
get {
return array[param];
}
set {
array[param] = value;
}
}
在靜態方法、靜態屬性訪問器和域宣告中的變數初始化器中使用this 是錯誤的。
下面的例子使用了this 來限制同名的類成員name 和alias。同時還將一個物件傳遞給另一個類中的
方法。
// keywords_this.cs
// this 示例
using System;
public class Employee {
public string name;
public string alias;
public decimal salary = 3000.00m;
// 構造器:
public Employee(string name, string alias) {
// 使用this 來限定name 和alias 域:
this.name = name;
this.alias = alias;
}
// 列印方法:
public void printEmployee() {
Console.WriteLine("Name: {0}/nAlias: {1}", name, alias);
// 通過使用this 將當前物件傳遞給CalTax()方法:
Console.WriteLine("Taxes: {0:C}", Tax.CalcTax(this));
}
}
public class Tax {
public static decimal CalcTax(Employee E) {
return (0.08m * (E.salary));
}
}
public class MainClass {
public static void Main() {
// 建立物件:
Employee E1 = new Employee ("John M. Trainer", "jtrainer");
// 顯示結果:
E1.printEmployee();
}
}
輸出為:
Name: John M. Trainer
Alias: jtrainer
Taxes: $240.00
抽象類
抽象類是一種特殊的基類。除了通常的類成員,它們還帶有抽象類成員。抽象類成員是指沒有實現而只
有宣告的方法和屬性。所有直接從抽象類派生的類都必須實現所有這些抽象方法和屬性。
抽象方法不能例項化。這樣做[譯註:指例項化一個抽象類]是不合邏輯的,因為那些抽象成員沒有實現。
那麼,不能例項化一個類有什麼好處呢?很多!抽象類穩坐類繼承樹的頂端。它們確定了類的結構和程式碼
的意圖。用它們可以得到更易搭建的框架。這是可能的,因為抽象類具有這個框架中所有基類的一般資訊
和行為。看看下面的例子:
abstract public class Contact { // 抽象類Contact
protected string name;
public Contact() {
// 語句
}
public abstract void generateReport();
abstract public string Name {
get;
set;
}
}
Contact 是一個抽象類。Contact 有兩個抽象成員,其中有一個是抽象方法,名為
generateReport()。這個方法使用了abstract 修飾符進行宣告,這個宣告沒有實現(沒有花括號)
並以分號結束。屬性Name 也被宣告為抽象的。屬性訪問器也是以分號結束。
public class Customer : Contact { // Customer 繼承自抽象類Contact
string gender;
decimal income;
int nuberOfVisits;
public Customer() {
// 語句
}
public override void generateReport() {
// 產生一份獨特的報告
}
public override string Name {
get {
numberOfVisits++;
return name;
}
set {
name = value;
nuberOfVisits = 0;
}
}
}
public class SiteOwner : Contact {
int siteHits;
string mySite;
public SiteOwner() {
// 語句
}
public override void generateReport() {
// 產生一份獨特的報告
}
public override string Name {
get {
siteHits++;
return name;
}
set {
name = value;
siteHits = 0;
}
}
}
抽象基類有兩個派生類——Customer 和SiteOwner。這些派生類都實現了基類Contact 中的抽象
成員。每個派生類中的generateReport()方法宣告中都有一個override 修飾符。同樣,Customer
和SiteOwner 中的Name 屬性的宣告也都帶有override 修飾符。當重寫方法時,C#有意地要求顯式的
宣告。這種方法可以跳程式碼的安全性,因為它可以防止意外的方法重寫,這在其他語言中確實發生過。省
略override 修飾符是錯誤的。同樣,新增new 修飾符也是錯誤的。抽象方法必須被重寫,不能隱藏。因
此既不能使用new 修飾符,也不能沒有修飾符。
所有抽象類中最出名的就是Object 類[譯註:.NET Framework 中的Object 類並不是抽象類]。它可以
寫作object 或Object[譯註:object 是C#中的關鍵字,用於宣告Object 類的一個物件;Object 是
指.NET Framework 類庫中的System.Object 類],但它們都是同一個類。Object 類是C#中所有其他類
的基類。它同時也是沒有指定基類時的預設基類。下面的這些類宣告產生同樣的結果:
abstract public class Contact : Object {
// 類成員
}
abstract public class Contact {
// 類成員
}
如果沒有宣告基類,Object 會隱式地成為基類。除了將C#類框架中的所有類聯絡在一起,Object 類
還提供了一些內建的功能,這些需要派生類來實現。
介面和抽象類之間的區別
介面和抽象類關係很緊密,它們都具有對成員的抽象。
對於一個抽象類,至少一個方法是抽象方法既可,這意味著它也可以具有具體方法[譯註:Concrete
Method,這只是相對於抽象方法而言,面向物件中並沒有這個概念]。
對於一個介面,所有的方法必須都是抽象的。
實現了一個介面的類必須為介面中的所有方法提供具體的實現,否則只能宣告為抽象類。
在C#中,多重繼承(Multiple Inheritance)只能通過實現多個介面得到。抽象類只能單繼承[譯註:C#中
的單繼承是指所有類作為基類的時候都只能是派生類宣告中唯一的基類,而不僅僅是抽象類]。
介面定義的是一個契約,其中只能包含四種實體,即方法、屬性、事件和索引器。因此介面不能包含常
數(Constant)、域、操作符、構造器、析構器、靜態構造器或型別[譯註:指巢狀的型別]。
同時,一個介面還不能包含任何型別的靜態成員。修飾符abstract、public、protected、internal、
private、virtual、override 都是不允許出現的,因為它們在這種環境中是沒有意義的。
類中實現的介面成員必須具有公有的可訪問性。
重寫概述
派生類可以通過override 關鍵字來重寫基類中的虛擬方法。這樣做必須遵守下面的約束:
? 關鍵字override 用於子類方法的定義,說明這個方法將要重寫基類中的虛擬方法。
? 返回值型別必須與基類中的虛擬方法一致。
? 方法的名字必須相同。
? 引數列表中的引數順序、數量和型別必須一致。
重寫的方法的可訪問性不能比基類中的虛擬方法具有更多的限制。可訪問性應具有相同或更少的限制[譯
注:實際上,在C#中重寫的方法的可訪問性必須與基類中的虛擬方法一致]。
子類中重寫的虛擬方法可以通過sealed 關鍵字宣告為封閉(Sealed)的,以防止在以後的派生類中改
變虛擬方法的實現。
隱藏基類成員
有的時候派生了的成員和基類中相應的成員具有相同的名字。這時,我們稱派生類需要“隱藏(Hiding)”
基類成員。
當發生隱藏時,派生類的成員將取代基類成員的功能。派生類的使用者將無法看到被隱藏的成員,他們
只能看到派生類的成員。下面的例子顯示瞭如何隱藏一個基類成員。
abstract public class Contact {
private string address;
private string city;
private string state;
private string zip:
public string FullAddress() {
string fullAddress = address + ‘/n’ + city + ‘,’ + state + ‘ ’ + zip;
return fullAddress;
}
}
public class SiteOwner : Contect {
public string FullAddress() {
string fullAddress;
// 建立一個地址...
return fullAddress;
}
}
在這個例子中,SiteOwner 和他的基類——Contact——都有一個名為FullAddress()的方法。
StieOwner 類中的FullAddress()方法隱藏了Contact 類中的FullAddress()方法。這意味著當調
用一個SiteOwner 類的例項的FullAddress()時,呼叫的是SiteOwner 類的FullAddress()方法,
而不是Contact 類的FullAddress()方法。
儘管基類中的成員可以被隱藏,派生類還是可以通過base 關鍵字來訪問它。有的時候這樣做是值得的。
這對於既要利用基類的功能又要新增派生類的程式碼是很有用的。下面的例子展示瞭如何在派生類中引用基
類中(被隱藏的)成員。
abstract public class Contact {
private string address;
private string city;
private string state;
private string zip;
public string FullAddress() {
string fullAddress = address + '/n' + city + ',' + state + ' ' + zip;
return fullAddress;
}
}
public class SiteOwner : Contact {
public string FullAddress() {
string fullAddress = base.FullAddress();
// 執行一些其它操作
return fullAddress;
}
}
在這個特定的例子中,SiteOwner 類中的FullAddress()方法呼叫了Contact 類中的
FullAddress()方法。這通過一個對基類的引用來完成。這提供了另一種重用程式碼的途徑,並添加了用
戶的行為。
版本
版本——繼承中的一個環境——在C#中是一種機制,可以修正類(建立新版本)但不致意外地改變了
程式碼的意圖。上面的隱藏基類成員方法的例子會得到編譯器的一個警告,這正是由於C#的版本政策。它[譯
注:指版本機制]被設計用於消除修正基類時所帶來的一些問題。
考慮這樣一幕:一個開發人員建立了一個類,從第三方的庫中的類繼承而來。最為討論,我們不妨假設
Contact 類就是第三方類庫中的類。看下面的例子:
public class Contact {
// 不包括FullAddress()方法
}
public class SiteOwner : Contact {
public string FullAddress() {
string fullAddress = mySite.ToString();
return fullAddress;
}
}
在這個例子中,基類中並沒有FullAddress()方法。這還沒有問題。稍後,第三方庫的建立者更新了
他們的程式碼。這些更新的部分中就包括了和派生類中的成員同名的成員:
public class Contact {
private string address;
private string city;
private string state;
private string zip;
public string FullAddress() {
string fullAddress = address + '/n' + city + ',' + state + ' ' + zip;
return fullAddress;
}
}
public class SiteOwner : Contact {
public string FullAddress() {
string fullAddress = mySite.ToString();
return fullAddress;
}
}
在這段程式碼中,基類中的FullAddress()方法和派生類中的方法具有不同的功能。在其他語言中,由
於隱式的多型性,這種情形將會破壞程式碼。然而,在C#中這不會破壞任何程式碼,因為當在SiteOwner 上
呼叫FullAdress()時,仍然呼叫的是SiteOwner 類中的方法。
但是這種情況會得到一個警告資訊。消除這一警告訊息的方法是在派生類的方法名字前面放置一個new
修飾符,如下面例子所示:
using System;
public class WebSite {
public string SiteName;
public string URL;
public string Description;
public WebSite() {
}
public WebSite (
string strSiteName,
string strURL,
string strDescription
) {
SiteName = strSiteName;
URL = strURL;
Description = strDescription;
}
public override string ToString() {
return SiteName + ", " + URL + ", " + Description;
}
}
public class Contact {
public string address;
public string city;
public string state;
public string zip;
public string FullAddress() {
string fullAddress = address + '/n' + city + ',' + state + ' ' + zip;
return fullAddress;
}
}
public class SiteOwner : Contact {
int siteHits;
string name;
WebSite mySite;
public SiteOwner() {
mySite = new WebSite();
siteHits = 0;
}
public SiteOwner(string aName, WebSite aSite) {
mySite = new WebSite (
aSite.SiteName,
aSite.URL,
aSite.Description
);
Name = aName;
}
new public string FullAddress() {
string fullAddress = mySite.ToString();
return fullAddress;
}
public string Name {
get {
siteHits++;
return name;
}
set {
name = value;
siteHits = 0;
}
}
}
public class Test {
public static void Main() {
WebSite mySite = new WebSite (
"Le Financier",
"http://www.LeFinancier.com",
"Fancy Financial Site"
);
SiteOwner anOwner = new SiteOwner("John Doe", mySite);
string address;
anOwner.address = "123 Lane Lane";
anOwner.city = "Some Town";
anOwner.state = "HI";
anOwner.zip = "45678";
address = anOwner.FullAddress(); // 不同的結果
Console.WriteLine("Address: /n{0}/n", address);
}
}
輸出為:
Address:
Le Financier, http://www.LeFinancier.com, Fancy Financial Site
這具有讓編譯器知道開發者意圖的效果。將new 修飾符放到基類成員宣告的前面表示開發者知道基類中
有一個同名的方法,並且他們確實想隱藏這個成員。這可以保護那些依賴於基類成員實現的現存程式碼不受
破壞。在C#中,當使用一個派生類物件時,呼叫的是基類的方法。同樣,當呼叫基類成員的方法時,呼叫
的也是基類的方法。但這會出現一個問題,就是如果基類中添加了一個重要的新特性,則這些新特性對派
生類是無效的。
要想使用這些新特性,就需要一些不同的途徑。一種選擇就是重新命名派生類的成員,以允許程式通過派
生類成員來使用一個基類方法。這種做法的缺點是,另一個依賴這個派生類實現的類可能具有同名的成員。
這會破壞程式碼,因此,這是一種不好的形式。
另一個選擇是在派生類中定義一個新的方法來呼叫基類方法。這允許派生類的使用者能夠獲得基類的新功
能,同時保留了派生類現有的功能。儘管這可以工作,但會關係到派生類的可維護性。
封閉類
封閉類是不可以繼承的類。將一個類宣告為封閉類可以保護這個類不被其他類繼承。這樣做有很多有益
的原因,包括優化和安全性。
封閉一個類可以皮面虛擬方法帶來的系統開銷。這允許編譯器進行一些優化,這些優化對普通的類是無
效的。
使用封閉類的另一個有益的原因是安全性。繼承,由於它的本質,可能以某些受保護成員能夠訪問到基
類的內部。封閉一個類可以排除派生類的這些缺陷。關於封閉類的一個很好的例子是String 類。下面的
例子顯示瞭如何建立一個封閉類:
public sealed class CustomerStats {
string gender;
decimal income;
int numberOfVisits;
public CustomerStats() {
}
}
public class CustomerInfo : CustomerStats { // 錯誤
}
這個例子將會產生編譯錯誤。由於CustomerStats 類是封閉的,它不能被CustomerInfo 類繼承。
不過CustomerStats 類可以用作一個類內部的物件。下面的就是在Customer 類中聲明瞭一個
CustomerStats 類的物件。
public class Customer {
CustomerStats myStats; // 正確
}
多型性
多型性反映了能夠在多於一個類的物件中完成同一事物的能力——用同一種方法在不同的類中處理不
同的物件。例如,如果Customer 和Vendor 物件都有一個Name 屬性,則我們可以寫一個事物來呼叫
Name 屬性而不管我們所使用的是Customer 物件還是Vendor 物件,這就是多型性。
交通工具是多型性的一個很好的例子。一個交通工具介面可以只包括所有交通工具都具有的屬性和方
法,還可能包括顏色、車門數、變速器和點火器等。這些屬性可以用於所有型別的交通工具,包括轎車、
卡車和掛車。
多型性不在交通工具的屬性和方法背後實現程式碼。相反,多型性只是實現介面。如果轎車、卡車和掛車
都實現了同樣的交通工具介面,則所有這三個類的客戶程式碼是完全一樣的。
C#通過繼承來為我們提供多型性。C#提供了virtual 關鍵字用於定義一個支援多型的方法。
子類對於虛擬方法可以自由地提供它自己的實現,這稱為重寫。下面是一些關於虛擬方法的要點:
要點:
? 如果方法是非虛擬的,編譯器簡單地使用所引用的型別來呼叫適當的方法。
? 如果方法是虛擬的,編譯器將產生程式碼來在執行時檢查所引用的類,來從適當的型別中呼叫適當
的方法。
? 當一個虛擬方法被呼叫時,執行時會進行檢查(方法遲繫結)來確定物件和呼叫適當的方法,所
有這些都是在執行時進行的。
對於非虛擬方法,這些資訊在編譯期間都是無效的,因此不會引起執行時檢查,所以呼叫非虛擬方法的
效率會略微高一些。但在很多時候虛擬方法的行為會更有用,損失的那些效能所換來的功能是很值得的。
實現多型性
實現多型性的關鍵因素是基於物件的型別動態地呼叫類的方法。本質上,一個程式會有一組物件,它會
檢查每一個物件的型別,並執行適當的方法。下面是一個例子:
using System;
public class WebSite {
public string SiteName;
public string URL;
public string Description;
public WebSite() {
}
public WebSite (
string strSiteName,
string strURL,
string strDescription
) {
SiteName = strSiteName;
URL = strURL;
Description = strDescription;
}
public override string ToString() {
return SiteName + ", " + URL + ", " + Description;
}
}
當我們繼承了上述的類,我們有兩種方法來呼叫這個類的構造器。因此,這是一個設計時的多型。這時,
我們必須在設計時決定在繼承的類中呼叫哪一個方法。
多型性是程式通過一個一般基類的引用來完成實現在多個派生類中的方法的能力。多型性的另一個定義
是通過同一種方法來處理不同物件的能力。這意味著檢測一個物件的行為是通過執行時型別,而不是它在
設計時所引用的型別。
總結
上面我嘗試通過一些實際的例子解釋了C#中面向物件的基本概念,但這將是一個長期的旅程。
相關推薦
C++中面向物件的思想
C++語言是C語言的拓展,C語言是面向過程的,C++在C的基礎上增加了面向物件的方法。 所謂面向過程的程式設計思想,就是分析解決問題的步驟,將這些步驟用一個個函式實現,最後一個個呼叫。 所謂面向物件的程式設計思想,就是將任何事物都
C++中面向物件模型初探
C++中面向物件模型即類的封裝原理初探這裡主要概述成員函式的本質,這裡只是用C語言的方式來實現C++中類的功能,並不代表C++編譯器的真正做法,但C++編譯器實現原理大致如此。/*註釋部分為c程式碼的實現方式,也是C++編譯器的實現原理*/ #include <iost
C#中的面向物件概念
C#中的面向物件概念原著:Tushar Kant Agrawal 12/24/2003原文:http://www.c-sharpcorner.com/Code/2003/Dec/OOPS In CSharp 1.0.asp翻譯:lover_P出處:http://www.cstc.net.cn/docs/doc
Python中面向物件的概念
1、語言的分類 1)面向機器 抽象成機器指令,機器容易理解。代表:組合語言。 2)面向過程 做一件事,排除步驟,第一步做什麼,第二步做什麼,如果出現A問題,做什麼處理,出現b問題,做什麼處理。問題規模小,步驟化,按部就班處理。 代表:c語言。 (按照步驟進行處理的。) 面向物件和麵向過
Java中面向物件的程式設計概念
面向物件的程式設計概念 -java學習之路 一、物件 在面向物件的程式設計設計中,物件當然是最基本的概念。不知道何為物件,怎麼面向物件呢。 物件一詞最早來源於哲學,講到“一切皆物件”,在這裡不討論哲學的問題了。在生活中,我們可以看到很多物件的例
C#中面向對象編程機制之多態學習筆記
tel codes var pub tools 不同 線程同步 dddddd 圖形 C#的多態性: 我的理解是:同一個操作,作用於不同的對象時,會有不同的結果,即同一個方法根據需要,作用於不同的對象時,會有不同的實現。 C#的多態包括:接口多態,繼承多態。 其中繼
Python3中面向物件------繼承
以下是我對Python3面向物件------繼承的理解,因為博主也是初學Python3,有很多東西都還停留在表層的理解,如果我的部落格有任何錯誤,請及時評論或者私信我,我會及時更改。也歡迎同樣學習Python的你願意關注我的部落格,我會把我每週的學習內容進行整
C++:面向物件程式設計
面向物件程式設計(OOP)基於三個基本概念:資料抽象、繼承和動態繫結(即封裝、繼承、多型)。 通過使用資料抽象,我們可以將類的介面與實現分離;使用繼承,可以定義相似的型別並對其相似關係建模;使用動態繫結,可以在一定程度上忽略相似型別的區別,而以統一的方式使用它們的物件。 動態繫結
C++Primer_Chap15_面向物件程式設計_List08_容器和繼承_筆記
當我們使用容器存放繼承體系中的物件時,通常必須採用間接儲存的方式。因為不允許在容器中儲存不同型別的元素,所以不能把具有繼承關係的多種型別的物件之間存放在容器中。 在容器中放置(智慧)指標而非物件 vector<shared_ptr<Quote>> bas
C++Primer_Chap15_面向物件程式設計_List07_建構函式與拷貝控制_筆記
虛解構函式 虛解構函式由於動態繫結可以確保delete基類指標時將執行正確的解構函式版本。一般,如果一個類需要解構函式,那麼它同樣需要拷貝和賦值操作。但基類的解構函式不遵循上述準則,是一個重要的例外。 虛解構函式將組織合成移動操作。如果一個類定義了解構函
C++Primer_Chap15_面向物件程式設計_List03_04_05_06_筆記
虛擬函式 由於當使用基類的引用或指標呼叫一個虛成員函式時會執行動態繫結。因為直到執行時才知道到底呼叫了哪個版本的虛擬函式,所以所有虛擬函式都必須有定義。 動態繫結只有當我們通過指標或引用呼叫虛擬函式才會發生。如果我們通過一個具有普通型別(非引用非指標)
C++Primer_Chap15_面向物件程式設計_List02_定義基類和派生類_筆記
class Quote{ public: Quote() = default; Quote(const std::string &book, double sales_price): bookNo(book), price(sales_price){} std::str
C++Primer_Chap15_面向物件程式設計_List01_OOP:概述_筆記
面向物件程式設計(object-oriented programming)的核心思想是資料抽象、繼承和動態繫結。 使用資料抽象,可以將類的介面與實現分離 使用繼承,可以定義相似的型別並對其相似關係建模 使用動態繫結,可以一定程度上忽略相似型別的區
python--面向物件概念
思路: 1.在完成一個需求前,首先確定職責–要做的事情(方法) 2.根據職責不同,指定不同的物件,在物件內部封裝不同的方法(多個) 3.最後完成的程式碼,就是順序地讓不同的物件呼叫不同的方法 一、類 概念:是對一群具有相同特徵或者行為的事物的統稱,是抽象
C語言筆記18--C語言面向物件程式設計
C語言是一門面向過程的程式語言,裡面沒有類的說法,沒有類的繼承、封裝、多型。Cpp是有類的概念的,Cpp本身就來源C語言,Cpp的類就是一個經過高度封裝的C語言結構體。在學習Cpp之前,瞭解C語言的設計模式非常重要,今天就用C語言結構體進行簡單的繼承、封裝、多型。 1.封裝 面向物件程式設計
PHP面向物件程式設計:面向物件概念、基本實踐、高階實戰、PHP面向物件特殊實踐
一、面向物件的概念 1.1 什麼是面向物件(object oriented) 世間萬物皆物件,抽象的也是物件,一切可見或不可見都是物件 1.2 物件的基本組成  
Scala中面向物件程式設計之trait
1.1將trait作為介面使用 Scala中的trait是一種特殊的概念; 首先先將trait作為介面使用,此時的trait就與Java中的介面 (interface)非常類似; 在trait中可以定義抽象方法,就像抽象類中的抽象方法一樣,只要不給出方法的方法體即可; 類可以使用ex
C語言面向物件程式設計:配置檔案解析(6)
在實際專案中,經常會把軟體的某些選項寫入配置檔案。 Windows 平臺上的 INI 檔案格式簡單易用,本篇文章利用《C語言面向物件程式設計(五):單鏈表實現》中實現的單鏈表,設計了一個“類” ini_parser 來讀寫 INI 格式的配置檔案。  
C語言面向物件程式設計:單鏈表實現(5)
前面我們介紹瞭如何在 C 語言中引入面嚮物件語言的一些特性來進行面向物件程式設計,從本篇開始,我們使用前面提到的技巧,陸續實現幾個例子,最後呢,會提供一個基本的 http server 實現(使用 libevent )。在這篇文章裡,我們實現一個通用的資料結構:單鏈表。