原著:Tushar Kant Agrawal 12/24/2003
原文:http://www.c-sharpcorner.com/Code/2003/Dec/OOPS In CSharp 1.0.asp
翻譯:lover_P
在這篇文章中我們将要讨論一些面向對象在C#中的具體實踐的關鍵概念。我們将要讨論一下面向對象的基礎包括接口(Interface)、通路修飾符(Access Modifier)、繼承(Inheritance)、多态(Polymorphism)等等。
面向對象的關鍵概念
抽象(Abstraction)
封裝(Encapsulation)
多态(Polymorphism)
繼承(Inheritance)
抽象
抽象是一種将一個對象歸納為一個資料類型的能力,這個資料類型具有特定的一組特征(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#中我們使用internal,而在Visual Basic .NET中我們使用Friend。
受保護内部(Protected Internal)成員隻能由包含了基類的應用程式中的從該基類派生的子類所通路。當你希望一個類中的成員隻能由其子類通路,并且拒絕其他應用程式通路該類的這個成員的時候,你就要将其聲明未受保護内部成員。
對象的成分
我們使用接口來獲得對對象的資料和行為的通路。對象的資料和行為包含在對象内部,是以,一個客戶應用程式可以将對象視為黑盒,隻有通過它的接口才能獲得可通路性。這是面向對象的關鍵概念,稱為封裝。這意味着任何使用這個對象的應用程式不能直接通路它的行為和資料——必須使用對象的接口。
對象有三個主要部分:
接口(Interface)
實作(Implementation)或行為(Behavior)
成員(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方法。
// Accessing base class members
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() {
// Calling the base class GetInfo method:
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;
// This constructor will call MyBase.MyBase()
public MyDerived(int ii) : base() {
}
// This constructor will call MyBase.MyBase(int i)
public MyDerived() : base(i) {
}
public static void Main() {
MyDerived md = new MyDerived(); // calls public MyDerived() : base(i)
// and passes i = 32 in base class
MyDerived md1 = new MyDerived(1); // calls 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();
// do some other stuff...
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() {
// Implementation
}
}
現在你就不能使用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 example
using System;
public class Employee {
public string name;
public string alias;
public decimal salary = 3000.00m;
// Constructor:
public Employee(string name, string alias) {
// Use this to qualify the fields, name and alias:
this.name = name;
this.alias = alias;
}
// Printing method:
public void printEmployee() {
Console.WriteLine("Name: {0}/nAlias: {1}", name, alias);
// Passing the object to the CalcTax method by using this:
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() {
// Create objects:
Employee E1 = new Employee ("John M. Trainer", "jtrainer");
// Display results:
E1.printEmployee();
}
}
輸出為:
Name: John M. Trainer
Alias: jtrainer
Taxes: $240.00
抽象類
抽象類是一種特殊的基類。除了通常的類成員,它們還帶有抽象類成員。抽象類成員是指沒有實作而隻有聲明的方法和屬性。所有直接從抽象類派生的類都必須實作所有這些抽象方法和屬性。
抽象方法不能執行個體化。這樣做[譯注:指執行個體化一個抽象類]是不合邏輯的,因為那些抽象成員沒有實作。那麼,不能執行個體化一個類有什麼好處呢?很多!抽象類穩坐類繼承樹的頂端。它們确定了類的結構和代碼的意圖。用它們可以得到更易搭建的架構。這是可能的,因為抽象類具有這個架構中所有基類的一般資訊和行為。看看下面的例子:
abstract public class Contact { // Abstract Class Contact
protected string name;
public Contact() {
// statements...
}
public abstract void generateReport();
abstract public string Name {
get;
set;
}
}
Contact是一個抽象類。Contact有兩個抽象成員,其中有一個是抽象方法,名為generateReport()。這個方法使用了abstract修飾符進行聲明,這個聲明沒有實作(沒有花括号)并以分号結束。屬性Name也被聲明為抽象的。屬性通路器也是以分号結束。
public class Customer : Contact { // Customer Inherits Abstract Class Contact
string gender;
decimal income;
int nuberOfVisits;
public Customer() {
// statements
}
public override void generateReport() {
// unique report
}
public override string Name {
get {
numberOfVisits++;
return name;
}
set {
name = value;
nuberOfVisits = 0;
}
}
}
public class SiteOwner : Contact {
int siteHits;
string mySite;
public SiteOwner() {
// statements...
}
public override void generateReport() {
// unique report
}
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 {
// class members
}
abstract public class Contact {
// class members
}
如果沒有聲明基類,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;
// create an address ...
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();
// do some other stuff...
return fullAddress;
}
}
在這個特定的例子中,SiteOwner類中的FullAddress()方法調用了Contact類中的FullAddress()方法。這通過一個對基類的引用來完成。這提供了另一種重用代碼的途徑,并添加了使用者的行為。
版本
版本——繼承中的一個環境——在C#中是一種機制,可以修正類(建立新版本)但不緻意外地改變了代碼的意圖。上面的隐藏基類成員方法的例子會得到編譯器的一個警告,這正是由于C#的版本政策。它[譯注:指版本機制]被設計用于消除修正基類時所帶來的一些問題。
考慮這樣一幕:一個開發人員建立了一個類,從第三方的庫中的類繼承而來。最為讨論,我們不妨假設Contact類就是第三方類庫中的類。看下面的例子:
public class Contact {
// does not include FullAddress() method
}
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'>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(); // Different Results
Console.WriteLine("Address: /n{0}/n", address);
}
}
輸出為:
Address:
Le Financier, http://www.LeFinancier.com'>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 { // Error
}
這個例子将會産生編譯錯誤。由于CustomerStats類是封閉的,它不能被CustomerInfo類繼承。不過CustomerStats類可以用作一個類内部的對象。下面的就是在Customer類中聲明了一個CustomerStats類的對象。
public class Customer {
CustomerStats myStats; // OK
}
多态性
多态性反映了能夠在多于一個類的對象中完成同一事物的能力——用同一種方法在不同的類中處理不同的對象。例如,如果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#中面向對象的基本概念,但這将是一個長期的旅程。
什麼是靜态?
在程式開發時,我們經常希望一個變量和方法不随對象的改變而改變,甚至在沒有建立對象時也能通路資料和方法,這時就可以在資料和方法上加上Static關鍵字,被Static修飾的資料就叫做靜态變量(資料)而方法就叫靜态方法。靜态變量在記憶體中的位址是相同的,是以對于同一類的不同對象,它們靜态變量的值肯定是相同的。