C# 篇基礎知識3——面向對象編程


  面向過程的結構化編程,例如1972年美國貝爾研究所推出的C語言,這類編程方式重點放在在定函數上,將較大任務分解成若干小任務,每個小任務由函數實現,分而治之的思想,然而隨着軟件規模的不斷擴張,軟件的復雜程度空前提高,例如Vista系統代碼達到5000萬行,安裝光盤有2.5GB。這種情況下,面向過程的自頂向下按功能將軟件分解成不同模塊的開發方式,分解模塊時很難保持各模塊的獨立性,使程序員設計模塊時很難排除其他模塊的影響,要同時考慮眾多模塊,故隨着程序規模的擴大,需要記住的細節越來越多,到一定程序時就變得難以應付了。另外,數據與代碼相分離的情況下,程序員往往很難理清數據和操作之間的聯系。從而出現了軟件危機。面向對象編程(Object-Oriented Programming,OOP)面向對象編程(Object-Oriented Programming,OOP)是一種強有力的軟件開發方法,在這種方法中,數據和對數據的操作被封裝成“零件”,人們用這些零件組裝程序。面向對象編程的組織方式和人們認識現實世界的方式一致,符合人們的思維習慣,大大減輕程序員的思維負擔;同時它有助於控制軟件的復雜性,提高軟件的生產效率。所以面向對象方法得到廣泛應用,已成為目前最流行的軟件開發方法。20世紀80年代初,貝爾實驗室在C語言基礎上設計了C++語句,1995年Sun公司提出了Java語言,2000年微軟公布了C#語言。

  面向對象編程和結構化編程並不矛盾,實際上面向對象編程的局部就是由結構化程序單元組成的。

1.面向對象的基本概念

主要有類、封裝、接口和對象等。

類,每一類事物都有特定的屬性和行為,這些不同屬性和行為將各類事物區別開來,面向對象編程采用類的概念,將事物編寫成一個個的類,用數據表示事物的屬性,用函數實現事物的行為。

封裝,正如開車的無需知曉汽車內部結構和原理,它們已被汽車工程師封裝在汽車內部,僅需利用提供給司機的一個簡單使用接口,司機操縱方向盤和各種按鈕就可以靈活自如開動汽車。面向對象技術把事物屬性和行為的實現細節封裝在類中,形成一個個可重復使用的“零件”,這些“零件”可被成千上萬程序員在不必知曉其內部原理情況下使用。這樣程序員能夠充分利用他人編寫好的“零件”,將主要精力集中在自已專門的領域。

接口,人們通過類的接口使用類,程序員在編寫類時精心地為其設計接口,既為方便其它程序員使用,也有利於類的升級改造。

對象,類是一個抽象的概念,而對象是類的具體實例,例如人是一個類,李白、杜甫等都是對象。即類是抽象的概念,對象是真實的個體。

2.創建類和對象

使用Class ***{}的方式定義類,未在Class前添加public的類只能在相同命名空間內部使用,為了能夠在其它命名空間使用類,要在Class前添加public標志符。類的成員變量稱為Field,也即是字段,類的屬性用變量表示,類的行為用方法實現。類通過公有成員實現接口,讓界可以通過接口使用類的功能。

C#通過new 運算符創建對象,執行該語句時系統先為對象分配相應的內存空間,然后通過類的構造函數初始化類的成員變量(每個類都有一個默認的與類同名的構造函數),這種創建對象的過程叫做類的實例化。如果創建了同一個類的多個對象,則它們共享方法的代碼,但不共享數據成員,每個對象都會在內存中開辟新的空間來存儲自己的數據成員。C#3.0中加入新特性——對象構造器,使得對象的初始化工作變得格外簡單,我們可以采用類似於數組初始化的方式來初始化類的對象。比如:Cat doraemon = new Cat { name = "Doraemon", age = 8 };。

1)屬性

用公有方法讀寫變量不但可以對數據進行合法性檢查,而且提高了類的封裝性,一箭雙雕。這種專門用來讀寫數據的方法稱為訪問器(Assessor)訪問器雖然解決了變量 age 的訪問問題,但是人們還是習慣於把年齡作為一個變量對待,用方法訪問不符合人們的思維習慣。

為了解決這個問題,C#設計了一種特殊的語法——屬性(Property)。在屬性中,定義了get和set兩個訪問器,get訪問器用來讀取變量的值,set訪問器用來設置變量的值。set訪問器沒有聲明顯式參數,但它有一個名為value的隱式參數。屬性的運行方式和方法相似,因此屬性可以看作特殊的方法,但屬性的使用方式和變量完全相同。無論何時使用屬性,都會在后台隱式地調用get訪問器或set訪問器,並執行訪問器中的代碼。每個屬性背后都對應着一個變量,我們一般讓屬性和它所對應的變量同名,只是將首字母大寫,以示區別。比如變量age相對應屬性Age。有時候屬性很簡單,get和set里面只有取值和賦值,沒有其它邏輯代碼,這種屬性可以通過自動屬性來快速定義public int Name { get; set;}。這時編譯器會自動創建一個與Name屬性相關的私有變量(僅將屬性名首字母改成小寫)以及Name屬性的定義代碼。總之,自動屬性包含兩層含義,先創建一個私有變量,然后創建該私有變量的屬性。

2)構造函數

創建對象時,系統先為對象的成員變量分配內存,然后通過構造函數(Constructor)初始化對象的成員變量。

構造函數,當未定義構造函數時,編譯器會為每個類分配一個默認構造函數,它將未明確賦值的整型字段和字符串字段分別初始化為0和null。通過自定義帶參數的構造函數,帶參數的構造函數可以把類 的員變量初始化為指定的值。構造函數是一種特殊的函數,它必須和類同名,並且沒有返回類型(連void也沒有)。當我們自定義了構造函數后,默認構造函數就失效了,要想繼續使用無參數的構造函數,必須顯式定義。自定義構造函數的函數體也可以為空,這時系統會用默認值初始化類的變量成員。

析構函數和垃圾回收,不用的對象要及時刪除以釋放內存空間,在傳統的面向對象設計中用類的析構函數(Destructor)刪除對象。析構函數也與類同名,只是要在函數名前加符號~,它不能帶任何參數,也沒有返回值。由於定義類時編譯器會自動生成一個缺省的析構函數,所以一般情況下沒有必要編寫析構函數,而且由於C#設計了非常完善的垃圾回收機制,一般也不用向析構函數里添加代碼。析構函數通常用來釋放對象使用的非托管資源。當對象即將離開作用域時,系統自動調用對象的析構函數,釋放對象所占的資源。然而在大型的程序中,有時有些對象雖然不再使用了,但離作用域結束還有相當長的時間,在這期間,對象仍然占用內存,浪費資源。C#專門設計了一套回收資源的機制——垃圾回收器。當垃圾回收器確定某個對象已經無用時,就會自動刪除該對象,釋放內存空間。在這套機制下,內存自動回收,無需人工干預,解決了常常困擾C++程序員的“內存泄露”問題。總之在C#中刪除對象的工作是由垃圾回收器負責完成,析構函數通常用來釋放對象使用的非托管資源。

.NET類庫已經為我們提供了大量的類,提供了大量現成的“輪子”供我們用,例如DateTime、String等,這樣就不用重復地制造了。

3)靜態成員

類的靜態成員用來描述類的整體特征,靜態成員在內存中只有一份,為類的所有對象共享,聲明方式:public static int wolvesCount=0;。用 static 關鍵字修飾的變量稱為靜態變量,沒有用static 關鍵字修飾的變量稱為實例變量,在C#中,實例變量通過對象名引用,而靜態變量通過類名引用。

靜態方法,一般認為行為是由具體對象發出的,但在某些情況下,對象的概念非常模糊例如Math類,直接通過類名調用方法反而更符合人們思維。因此,可以將類的方法聲明靜態方法,靜態方法用static 關鍵字聲明,調用靜態方法時不必事先創立對象,直接通過類名引用。

4)常量成員

類的const常量只能在聲明的時候初始化,不能在其它地方賦值,聲明方式例如public const double PI=3.1415926;。類的const常量成員是隱式靜態的,為所有類對象共有,通過類名來引用,雖然const常量默認靜態,但不能用static關鍵字顯式聲明。

readonly常量,對於那些在類的具體對象中固定的常數,但在不同對象中可有不同值的變量,通常用readonly常量實現。例如旅店類的房間總數,不同旅店的房間總數不同,但一個旅店的房間總數通常是固定的。一般把readonly常量初始化代碼放在了構造函數。

5)重載

C#提供了方法重載的方式,即允許在一個類中定義兩個或多個名稱完全相同的方法,但要求這些名稱相同的方法必須具有不同的參數類型(參數數據類型不同或參數個數不同或同時具有這兩點)。方法重載調用原則是參數最佳匹配,即系統調用 參數類型最匹配的那個方法。例如通常在定義類的構造函數時使用方法重載。

重載運算符,重載運算符由關鍵字operator聲明,必須定義為靜態。定義方法:pubic static ***(返回類型) operator +(或-*/) (*** z1,***z2){…}。例如:

class Complex

{  public static Complex operator +(Complex z1, Complex z2)

{ return Add(z1, z2);}  …}

 

6)索引

使用類的索引,可以像訪問數組那樣訪問類的數據成員,定義類索引的方式就像以定義屬性的方式定義類的數組成員。例如:

class cube{

private double length;

private double width;

private double height;

public double this[int index]

{

get

{ switch(index)

{ case 0:return length;

   case 1:return width;

   case 2:return height:

   default: throw new IndexOutOfRangeException(“下標出界”);}

}

set{  switch(index)

     {case 0: lenth=value;break;

      case1 :width=value;break;

      case 2:height=value;break;

default: throw new IndexOutOfRangeException(“下標出界”);}

}

}

}

索引的函數體與屬性類似,也是用get 和set 訪問器。get 訪問器用於獲取成員變量的值,set 訪問器用於為成員變量賦值。索引的使用方法和數組完全一樣,如果創建了一個名為 box 的Cube 對象,就可以用box[0],box[1],box[2]分別表示立方體的長、寬、高了。在數組中,下標只能為整數,在索引中,有了更靈活的選擇,既可以為 int 型,也可以為double、string 等類型。例如:

class Cube

{

public double this[string indexString]

{

get

{ switch (indexString)

{  case "length": return length;

case "width": return width;

case "height": return height;

//當下標出界時,我們拋出一段異常。

default: throw new IndexOutOfRangeException("下標出界!");}

}

set

{ switch (indexString)

{  case "length": length = value; break;

case "width": width = value; break;

case "height": height = value; break;

default:throw new IndexOutOfRangeException("下標出界!");}

}

}

...

}

C#還為我們提供了多維索引,只需提供多個下標即可,比如Matrix 類中的多維索引public double this[int rowIndex,int columnIndex]這時需要嵌套的switch 語句或雙重循環語句等方式來實現。

3.值類型和引用類型

內存中有一塊稱為棧的區域,用來存儲整型、實型、布爾型和字符型等基本類型。操作系統通過棧指針中存儲的地址讀寫棧中的數據,當棧為空時,棧指針指向棧的底部,隨着數據的不斷入棧,棧指針不斷向棧頂部移動,但始終指向棧中下一塊自由空間。棧對數據的操作總發生在棧的頂部,最后入棧的變量最先彈出,最先入棧的數據最后彈出,因此先入棧數據的作用域總比后入棧的要長,后入棧數據的作用域嵌套在先入棧數據之中。棧的這種工作方式稱為后入先出(Last in first out,LIFO)。整型、實型、布爾型、字符型等簡單數據和結構體存儲在棧中,稱為值類型變量(Value type)。

引用型變量,在類中,我們希望成員變量被構造函數初始化后,即使退出構造函數,這些變量仍然存在,以便在需要的地方使用,為此C#把類的成員變量存儲在(Heap)上。內存中創建類的一個對象的過程分兩步:

第一步在堆中創建對象:系統在堆中划分一塊20 字節的空間用於存儲類對象的成員變量,並調用構造函數初始化它們,這樣一個對象就被創建了。

第二步在棧上創建引用符:系統在棧中分配4 字節的空間,存儲類對象在堆中的首地址。這種存儲於棧中的指向堆中對象的地址稱為引用符,系統通過引用符找到堆中的對象。

稱這種存儲在堆上的對象稱為引用型變量,引用型變量的垃圾回收機制:在實際程序中,可能會有多個引用符同時指向同一個對象,當某個引用符退出作用域時,系統就會從棧中刪除該引用符。當指向對象的所有引用符都被刪除時,對象就被加入垃圾回收的候選名單,垃圾回收器會在適當的時候清除該對象。只要存在指向對象的引用符,對象就不會被清除。.Net 使用的是托管堆,托管堆和傳統堆不同,當垃圾回收器清除一個對象后,垃圾回收器會移動堆中其他對象,使它們連續的排列在堆的底部,並用一個堆指針指向空閑空間。當創建新對象時,系統根據堆指針可以直接找到自由的內存空間。使用托管堆時創建對象要快很多,雖然刪除對象時整理操作會浪費一定的時間,但這些損失會在其它方面得到很好的補償。聲明一個對象的引用符但卻未通過new關鍵字為其創建一個對象時,引用符中存儲的只是空地址。注意記住:引用符是對象在內存中的地址。

4.對象的相等、對象數組、匿名類型、擴展方法

1)對象的相等

初次看到Object 類的成員你會感到很驚訝,竟然有三個“相等”函數,如果再加上相等運算符“==”的話,就有4 種進行相等比較的方式了。這些方式之間有什么區別呢?

靜態的ReferenceEquals()方法,是一個靜態方法,用來測試兩個引用符是否指向同一個對象,即兩個引用符是否包含相同的內存地址。實例版的Equals()方法,在 Object 類中,實例版Equals()方法是一個虛方法,如果未對其重載,只進行引用比較。但我們一般會在派生類中重寫該方法以實現值比較。相等運算符==,。默認狀態下,若兩個對象為值類型,相等運算符“==”比較兩個對象的值;若兩個對象為引用類型,相等運算符“==”比較兩個引用符。但我們可以通過重載運算符的方法加以改變。靜態版 Equals()方法的功能與實例版基本一樣,實際上靜態版Equals()方法通過調用實例版方法進行比較,只是在調用“objA.Equals(objB)”前,會先檢查兩個參數是否均為空引用,如果均為空引用,返回true,如果一個為空,一個不為空,返回false。總之,一般情況下,我們讓 ReferenceEquals()方法進行引用比較,Equals()方法進行值比較,而相等運算符“==”則看情況而定,如果對象看起來像一個值(比如復數類),就把它設計成值比較,如果對象看起來不像值,就把它設計成引用比較。

2)以對象為元素的數組

例如Cat [] cats=new Cat[5];如此僅是聲明了一組引用符而已,並沒有真正創建對象。也可以像普通數組那樣,聲明和初始化同時進行,Cat[] cats = new Cat[]{ new Cat("doraemon,8"), new Cat("Garfield",6) };。

(3)匿名類

有時候某些類在程序中只會使用一次,單獨為它編寫一個類顯得過於啰嗦,這時候用匿名類型就會簡潔很多,例如var city = new { Name = "Beijing", ZipCode = 100000 };匿名類型只能由一個或多個公共只讀屬性組成,不能包含其它種類的類成員。編譯時編譯器會自動生成相關的類,並創建一個該類的對象以供使用。

(4)擴展方法

很多時候,當需要在已經編譯好的類中添加新的功能,但又不想派生該類時,就要以為該類添加擴展方法。

public   static ReturnDataTypeX  ExtensionMethodName(this ExtendedClass x,DataTypeA y,DataTypeB z,…){}

可在任意命名空間的任意一個靜態類中為其它類擴展方法,擴展方法的第一個參數類型是想要擴展的類,必須用this關鍵字修飾,且擴展方法要定義為靜態方法。注意調用擴展方法時,只需要引入擴展方法所在的命名空間,通過被擴展類的實例對象,就可以調用這個擴展方法了。

5. 繼承

繼承(Inheritance)是軟件重用的一種形式,采用這種形式,可以在現有類的基礎上添加新的功能,從而創造出新的類。

1)由基類創建派生類

例如class Mammal:Vertebrata{},這里Mammal 類由Vertebrata 類派生而出,它不光具有自己的新成員,而且繼承了Vertebrata 類的所有成員。派生類Mammal 雖然繼承了基類Vertebrata 的所有成員,但出於封裝性的考慮,在派生類中不能使用基類的私有成員。

2protected成員

如果想讓類的成員既保持封裝性又可以在派生類中使用,那么可以把它定義為protected 成員(受保護成員)。總之private 成員只能在定義它的類中使用,既不能被外界使用,也不能被派生類使用;而protected 成員雖然不能被外界使用,但可以被派生類使用。

3)間接繼承

例如class Mammal:Vertebrata{}、class Human : Mammal{},那么Human就間接繼承了Vertebrata的所有非私有成員。

4)虛方法的重寫

考慮到派生類的某些方法雖然與基本同名,但卻想賦與其不同的方法內涵,這時就可以把基類的這些方法設計為虛方法,然后在派生類中重寫(Override)該方法。虛方法用virtual聲明。例如:

class Vertebrata { public virtual void Breathe() { Console.WriteLine("呼吸");}…}

class Mammal : Vertebrata { public override void Breathe() { Console.WriteLine("肺呼吸"); }…}

class Fish : Vertebrata{public override void Breathe(){Console.WriteLine("鰓呼吸");}…}

有三個版本的Breathe()方法,當對象屬於Vertebrata 類時,會調用Vertebrata

類中的Breathe()方法;當對象屬於Mammal 類時,會調用Mammal 類中的Breathe()方法;當對象屬於Fish 類時,會調用Fish 類中的Breathe()方法。總之,系統會根據對象的實際類型調用相應版本的方法。

不但可以重寫方法,也可以重寫屬性,但靜態方法不能重寫。重寫和重載的區別,很多讀者一直不清楚方法重載和方法重寫的區別,其實重載和重寫的區別很簡單。重

載是指同一個類中有若干個名稱相同但參數不同的方法,調用方法時,系統會根據實參情況,調用參數完全匹配的那個方法。重寫是指在繼承關系中,在派生類中重寫由基類繼承來的方法,這時基類和派生類中就有兩個同名的方法,系統根據對象的實際類型調用相應版本的方法,當對象類型為基類時,系統調用基類中的方法,當對象類型為派生類時,系統調用派生類中被重寫的方法,這其實體現了類的多態性。重載方法發生在同一個類中的同名方法之間,重寫方法發生在基類和派生類之間。

5)隱藏基類非virtual的普通方法

由於只能重寫基類中的虛方法,不能重寫普通方法。要想在派生類中修改基類的普通方法,需要用new 關鍵字隱藏基類中的方法。例如public new void Sleep(){…},如此可以隱藏父類的同名且非virtual方法。

6base關鍵字

如何在派生類中調用被重寫或隱藏的基類方法呢?我們知道,每個對象都可以用this關鍵字引用自身的成員,同樣,也可以用base關鍵字引用基類的成員。

7)抽象類和抽象方法

沒有實例化意義的類,例如脊椎動物只是一個抽象概念,每一種脊椎動物要么是魚類,要么是鳥類,要么是哺乳動物等,這樣的類可以設計成抽象類,抽象類不能被實例化,只能作為其它類的基類存在,其目的是抽象出子類的公共部分以減少代碼重復。抽象類用關鍵字abstract聲明。不光可以聲明抽象類,還可以聲明抽象方法。抽象方法是一種特殊的虛方法,它只能定義在抽象類中,抽象方法沒有任何執行代碼必須在派生類中用重寫的方式具體實現(如果忘記重寫抽象方法的話,會出現編譯錯誤)。除了抽象方法外,我們還可以在抽象類中定義抽象屬性(Abstract property)。抽象屬性也沒有具體實現代碼,必須在派生類中重寫。

8)密封類和密封方法

密封類(Sealed class)是一種不能被繼承的類,用sealed關鍵字聲明。同樣,如果想防止一個方法被派生類重寫,可以用sealed override關鍵字把它為聲明密封方法,例如public sealed override void F()。

9)派生類的構造函數

創建對象時,系統先調用基類的構造函數,初始化基類的變量,然后調用派生類的構造函數,初始化派生類的變量,是一個由基類向派生類逐步構建的過程。刪除對象時,先調用派生類的析構函數,銷毀派生類的變量,然后調用基類的析構函數,銷毀基類的變量,是一個由派生類向基類逐步銷毀的過程。

因為派生類不能使用基類的私有變量,所以不能通過派生類的構造函數直接初始化基類的私有變量,但我們可以通過顯式調用基類的構造函數實現。例如:public Human(string nameValue,int ageValue):base(ageValue)。一般情況下盡量用基類的構造函數初始化基類的成員。

10)萬類之源Object

C#中所有的類都直接或間接繼承於Object 類,C#專門設計了object 關鍵字,它相當於Object 的別名,該類定義了以下方法:

6.多態性

對象存儲在堆中,引用符存儲在棧中,引用符的值是對象在堆中的地址,因此通過引用符可以輕松的找到對象,一般情況下,引用符和對象屬於同一類型,基類的引用指向基類的對象,派生類的引用指向派生類的對象。基類引用符可以指向派生類對象,但派生類的引用符不能指向基類的對象。繼承和多態性是開發復雜軟件的關鍵技術。可以定義基類的引用符,針對此引用符調用基類的某些方法,后續通過不同的派生類對象“賦值”給這個引用符,從而在調用這個基類引用符方法時,使其實際調用派生類對象的不同方法,從而方法的調用呈現多態性。

is引用符,通過它用來判斷對象是否與給定的類型兼容(屬於該類型或者屬於該類型的子類)。所有派生類的對象都可以看作基類的對象。

向上或向下類型轉換,由派生類轉換為基類,即向上的類型轉換是自動進行的,但轉換后,基類的引用符不能引用派生類特有的成品。由基類向派生類轉換的過程為向下類型轉換,這時需要強制向下轉換,由於僅當由基類向派生類強制轉換才能成功,不然程序就會拋出異常,因此,轉換前,應當使用is運算符進行檢查,類型是否兼容,強制轉換可以通過as運算符實現,例如Vertebrata someone=new Human(); Human people=someone as Human; as轉換是執行兩個引用類型間的顯式轉換,是一種安全的轉換,使用前不需要使用is運行符測試類型,當類型不兼容時,轉換的結果是null,而不會拋出異常,因此通常建議采用as進行類型轉換。

7.接口

世界很多組織致力於標准化工作,標准化可以增強產品的通用性,當然也可增強軟件的通用性,使軟件使用更加方便。在軟件領域,實現標准化的一種方法是制定統一的接口(Interface)。接口只規定系統具有哪些成員以及每個成員的功能,具體如何實現由各個公司自己設計,也就是說接口為大家制定了一個規范,然后各公司具體實現這個規范。接口用關鍵字interface 定義,接口的名稱習慣上以字母I 開頭。一般情況下,接口中只能包含成員的聲明,不能有任何實現代碼。接口的成員總是公有的,不需要也不能添加public 等修飾符,也不能聲明為虛方法或靜態方法。注意,實現接口的類的相應成員必須添加public 修飾,並且可以聲明為虛方法。

(1)接口的繼承

接口繼承方式與類相同,接口跟接口實現類的關系與基類與派生類的關系一樣,因此通過使用接口聲明對象,也可以很好地使接口聲明對象呈現出多態性。

(2)接口的多繼承和顯式實現

C#中,類與類之間是單繼承的,即一個類只能繼承一個基類,但類與接口間是多繼承的,一個類可以實現多個接口。如果一個類繼承了多個接口而這些接口又有重名的成員,就必須用到接口的顯式實現,即通過在接口實現類中如此顯式實現:

interface IAnimal{ void Breathe();}

interface IPlant{ void Breathe();}

class Life:IAnimal,IPlant

{ void IAnimal.Breathe(){…}

  void IPlant.Breathe(){…}}

可以看出,顯式實現就在方法屬性前加上相應的接口名即可。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM