書名: C#入門經典(第7版):C# 6.0 & Visual Studio 2015(.NET開發經典名著)作者名: (美)Beijamin Perkins Jacob Vibe Hammer Jon D. Reid本章字數: 5342字更新時間: 2021-04-02 21:18:43
8.2 OOP技術
前面介紹了一些基礎知識,知道對象是什么,以及對象的工作原理,下面討論對象的其他一些特性,包括:
● 接口
● 繼承
● 多態性
● 對象之間的關系
● 運算符重載
● 事件
● 引用類型和值類型
8.2.1 接口
接口是把公共實例(非靜態)方法和屬性組合起來,以封裝特定功能的一個集合。一旦定義了接口,就可以在類中實現它。這樣,類就可以支持接口所指定的所有屬性和成員。
注意,接口不能單獨存在。不能像實例化一個類那樣實例化接口。另外,接口不能包含實現其成員的任何代碼,而只能定義成員本身。實現過程必須在實現接口的類中完成。
在前面的咖啡示例中,可以把通用屬性和方法,例如AddSugar()、Milk、Sugar和Instant組合到一個接口中,這個接口可以命名為IHotDrink(接口名一般以大寫字母I開頭)。然后就可以在其他對象上使用該接口,例如CupOfTea類的對象。所以可以采用類似方式處理這些對象,而對象仍保有自己的屬性(例如CupOfCoffee仍有屬性BeanType, CupOfTea仍有屬性LeafType)。
在UML中,在對象上實現的接口用“棒棒糖”語法來表示。在圖8-6中,用與類相似的語法把IHotDrink的成員放在一個單獨的框中。

圖8-6
一個類可以支持多個接口,多個類也可以支持相同的接口。所以接口的概念讓用戶和其他開發人員更容易理解其他人的代碼。例如,有一些代碼使用一個帶某接口的對象。假定不使用這個對象的其他屬性和方法,就可以用另一個對象代替這個對象(例如,使用上述IHotDrink接口的代碼可以處理CupOfCoffee和CupOfTea實例)。另外,該對象的開發人員可以提供該對象的更新版本,只要它支持已經在用的接口,就可以在代碼中使用這個新版本。
發布接口后,即接口可以用于其他開發人員或終端用戶后,最好不要修改它。理解這一點的一種方式是把接口看成類的創建者和使用者之間的協定,即“每個支持接口X的類都支持這些方法和屬性”。如果以后修改了接口,也許是升級了底層的代碼,該接口的使用者就不能正確運行接口,甚至失敗。所以,我們應做的是創建一個新接口,使其擴展舊接口,可能還包含一個版本號,如X2。這是創建接口的標準方式,以后我們會常常遇到已編號的接口。
可刪除的對象
IDisposable接口特別有趣。支持IDisposable接口的對象必須實現Dispose()方法,即它們必須提供這個方法的代碼。當不再需要某個對象(例如,在對象超出作用域之前)時,就調用這個方法,釋放重要資源,否則,等到對垃圾回收調用析構方法時才會釋放該資源。這樣可以更好地控制對象所用的資源。
C#允許使用一種可以優化使用這個方法的結構。using關鍵字可以在代碼塊中初始化使用重要資源的對象,在這個代碼塊的末尾會自動調用Dispose()方法,用法如下:
<ClassName> <VariableName> = new <ClassName>(); ... using (<VariableName>) { ... }
或者把初始化對象<VariableName>作為using語句的一部分:
using (<ClassName> <VariableName> = new <ClassName>()) { ... }
這兩種情況下,可在using代碼塊中使用變量<VariableName>,并在代碼塊的末尾自動刪除(在代碼塊執行完畢后,調用Dispose())。
8.2.2 繼承
繼承是OOP最重要的特性之一。任何類都可以從另一個類繼承,這就是說,這個類擁有它繼承的類的所有成員。在OOP中,被繼承(也稱為派生)的類稱為父類(也稱為基類)。注意,C#中的對象僅能直接派生于一個基類,當然基類也可以有自己的基類。
繼承性可從一個較一般的基類擴展或創建更多的特定類。例如,考慮一個代表農場家畜的類(由80多歲的資深開發人員MacDonald在他的家畜應用程序中使用)。這個類名為Animal,擁有EatFood()或Breed()等方法,我們可以創建一個派生類Cow; Cow支持所有這些方法,也有自己的方法,如Moo()和SupplyMilk()。還可以創建另一個派生類Chicken,該類有Cluck()和LayEgg()方法。
在UML中,用箭頭表示繼承,如圖8-7所示。

圖8-7
注意:為簡潔起見,圖8-7中省略了成員的返回類型。
在繼承一個基類時,成員的可訪問性就成了一個重要問題。派生類不能訪問基類的私有成員,但可以訪問其公共成員。不過,派生類和外部的代碼都可以訪問公共成員。這就是說,只使用這兩個級別的可訪問性,不能讓一個成員可由基類和派生類訪問,而不能由外部的代碼訪問。
為解決這個問題,C#提供了第三種可訪問性:protected,只有派生類才能訪問protected成員。對于外部代碼來說,這個可訪問性與私有成員一樣:外部代碼不能訪問private成員和protected成員。
除了定義成員的保護級別外,我們還可以為成員定義其繼承行為。基類的成員可以是虛擬的,也就是說,成員可以由繼承它的類重寫。派生類可以提供成員的另一種實現代碼。這種實現代碼不會刪除原來的代碼,仍可以在類中訪問原來的代碼,但外部代碼不能訪問它們。如果沒有提供其他實現方式,通過派生類使用成員的外部代碼就自動訪問基類中成員的實現代碼。
注意:虛擬成員不能是私有成員,因為這樣會自相矛盾——不能既要求派生類重寫成員,又不讓派生類訪問該成員。
在前面的家畜示例中,可以把EatFood()變成虛擬成員,在派生類中為它提供新的實現代碼,例如為Cow類提供新的實現代碼,如圖8-8所示。這里顯示了Animal和Cow類的EatFood()方法,說明它們有自己的實現代碼。

圖8-8
基類還可以定義為抽象類。抽象類不能直接實例化。要使用抽象類,必須繼承這個類,抽象類可以有抽象成員,這些成員在基類中沒有實現代碼,所以派生類必須實現它們。如果Animal是一個抽象類,UML就會如圖8-9所示。

圖8-9
注意:抽象類名以斜體顯示(有時它們的方框有一個短橫線)。
在圖8-9中,EatFood()和Breed()都顯示在派生類Chicken和Cow中,這說明這些方法是抽象的(必須在派生類中重寫)或者虛擬的(這里已經在Chicken和Cow中重寫)。當然,抽象基類可以提供成員的實現代碼,這是十分常見的。不能實例化抽象類,并不意味著不能在抽象類中封裝功能。
最后,類可以是密封(seal)的。密封的類不能用作基類,所以沒有派生類。
在C#中,所有對象都有一個共同的基類object(在.NET Framework中,它是System.Object類的別名)。第9章將詳細介紹這個類。
注意:如本章前面所述,接口也可以繼承自其他接口。與類不同的是,接口可以繼承多個基接口(與類可以支持多個接口的方式類似)。
8.2.3 多態性
繼承的一個結果是派生于基類的類在方法和屬性上有一定的重疊,因此,可以使用相同的語法處理從同一個基類實例化的對象。例如,如果基類Animal有一個EatFood()方法,則在其派生類Cow和Chicken中調用這個方法的語法是類似的:
Cow myCow = new Cow(); Chicken myChicken = new Chicken(); myCow.EatFood(); myChicken.EatFood();
多態性則更推進了一步。可以把某個派生類型的變量賦給基本類型的變量,例如:
Animal myAnimal = myCow;
不需要進行強制類型轉換,就可以通過這個變量調用基類的方法:
myAnimal.EatFood();
結果是調用派生類中的EatFood()的實現代碼。注意,不能以相同的方式調用派生類上定義的方法。下面的代碼無法運行:
myAnimal.Moo();
但可以把基本類型的變量轉換為派生類變量,調用派生類的方法,如下所示:
Cow myNewCow = (Cow)myAnimal; myNewCow.Moo();
如果原始變量的類型不是Cow或派生于Cow的類型,這個強制類型轉換就會引發一個異常。有許多方式說明對象的類型是什么,詳見下一章。
在派生于同一個類的不同對象上執行任務時,多態性是一種極有效的技巧,其使用的代碼最少。注意并不是只有共享同一個父類的類才能利用多態性。只要子類和孫子類在繼承層次結構中有一個相同的類,它們就可以用同樣的方式利用多態性。
還要注意,在C#中,所有類都派生于同一個類object, object是繼承層次結構中的根。所以可以把所有對象看成object類的實例。這就是在建立字符串時,WriteLine()可以處理無數多種參數組合的原因。第一個參數后面的每個參數都可以看成一個object實例,所以可以把任何對象的輸出結果寫到屏幕上。為此,需要調用方法ToString()(object的一個成員)。我們可以重寫這個方法,為自己的類提供合適的實現代碼,或者使用默認實現代碼,返回類名(根據它所在的名稱空間,返回類的限定名稱)。
接口的多態性
盡管不能像對象那樣實例化接口,但可以建立接口類型的變量,然后就可以在支持該接口的對象上,使用這個變量來訪問該接口提供的方法和屬性。
例如,假定不使用基類Animal提供的EatFood()方法,而是把該方法放在IConsume接口上。Cow和Chicken類也支持這個接口,唯一的區別是它們必須提供EatFood()方法的實現代碼(因為接口不包含實現代碼),接著就可以使用下述代碼訪問該方法了:
Cow myCow = new Cow(); Chicken myChicken = new Chicken(); IConsume consumeInterface; consumeInterface = myCow; consumeInterface.EatFood(); consumeInterface = myChicken; consumeInterface.EatFood();
這就提供了以相同方式訪問多個對象的簡單方式,且不依賴于一個公共的基類。例如,這個接口可以由派生于Vegetable而不是Animal的VenusFlyTrap類實現:
VenusFlyTrap myVenusFlyTrap = new VenusFlyTrap(); IConsume consumeInterface; consumeInterface = myVenusFlyTrap; consumeInterface.EatFood();
在這段代碼中,調用consumeInterface.EatFood()的結果是調用Cow、Chicken或VenusFlyTrap類的EatFood()方法,這取決于把哪個實例賦予接口類型的變量。
注意,派生類會繼承其基類支持的接口。在上面的第一個示例中,要么是Animal支持IConsume,要么是Cow和Chicken支持IConsume。有共同基類的類不一定有共同接口,反之亦然。
8.2.4 對象之間的關系
繼承是對象之間的一種簡單關系,可以讓派生類完整地獲得基類的特性,而且派生類也可以訪問基類內部的一些工作代碼(通過受保護的成員)。對象之間還具有其他一些重要關系。
本節簡要討論下述關系:
● 包含關系:一個類包含另一個類。這類似于繼承關系,但包含類可以控制對被包含類的成員的訪問,甚至在使用被包含類的成員前進行其他處理。
● 集合關系:一個類用作另一個類的多個實例的容器。這類似于對象數組,但集合具有其他功能,包括索引、排序和重新設置大小等。
1.包含關系
用一個成員字段包含對象實例,就可以實現包含(containment)關系。這個成員字段可以是公共字段,此時與繼承關系一樣,容器對象的用戶就可以訪問它的方法和屬性,但不能像繼承關系那樣,通過派生類訪問類的內部代碼。
另外,可以讓被包含的成員對象變成私有成員。如果這么做,用戶就不能直接訪問任何成員,即使這些成員是公共的。但可以使用包含類的成員訪問這些私有成員。也就是說,可以完全控制被包含的類對外提供什么成員(或者不提供任何成員),還可以在訪問被包含類的成員前,在包含類的成員上執行其他處理。
例如,Cow類包含一個Udder類,Udder類有一個公共方法Milk()。Cow對象可以按照要求調用這個方法,作為其SupplyMilk()方法的一部分,但Cow對象的用戶看不到這些細節,或者這些細節對Cow對象的用戶并不重要。
在UML中,被包含類可以用關聯線條來表示。對于簡單包含關系,可以用帶有1的線條說明一對一的關系(一個Cow實例包含一個Udder實例)。為清晰起見,也可以把被包含的Udder類實例表示為Cow類的私有字段,如圖8-10所示。
2.集合關系

圖8-10
第5章討論了如何使用數組存儲多個同類型變量,這也適用于對象(前面使用的變量類型實際上是對象)。例如:
Animal[] animals = new Animal[5];
集合基本上就是一個增加了功能的數組。集合以與其他對象相同的方式實現為類。它們通常以所存儲的對象名稱的復數形式來命名,例如用類Animals包含Animal對象的一個集合。
數組與集合的主要區別是,集合通常實現額外的功能,例如Add()和Remove()方法可添加和刪除集合中的項。而且集合通常有一個Item屬性,它根據對象的索引返回該對象。通常,這個屬性還允許實現更復雜的訪問方式。例如,可以設計一個Animals,讓Animal對象根據其名稱來訪問。
其UML表示如圖8-11所示。圖8-11中沒有包含成員,因為這里描述的是關系。連接線末尾的數字表示一個Animals對象可以包含0個或多個Animal對象。第11章將詳細論述集合。

圖 8-11
8.2.5 運算符重載
本書前面介紹了如何使用運算符處理簡單的變量類型。有時也可以把運算符用于從類實例化而來的對象,因為類可以包含如何處理運算符的指令。
例如,給Animal添加一個新屬性Weight。接著使用下述代碼比較家畜的體重:
if (cowA.Weight > cowB.Weight)
{
...
}
使用運算符重載,可在代碼中提供隱式使用Weight屬性的邏輯,如下面的代碼所示:
if (cowA > cowB) { ... }
大于運算符>被重載了。我們為重載運算符編寫代碼,執行上述操作,這段代碼用作類定義的一部分,而該運算符作用于這個類。在上面的示例中,使用了兩個Cow對象,所以運算符重載定義包含在Cow類中。也可以采用相同的方式重載運算符,使其處理不同的類,其中一個(或兩個)類定義包含達到這一目的的代碼。
注意,只能采用這種方式重載現有的C#運算符,不能創建新的運算符。但可以為一元和二元運算符(如+或>)提供實現代碼。詳見第13章。
8.2.6 事件
對象可以激活和使用事件,作為它們處理的一部分。事件是非常重要的,可以在代碼的其他部分起作用,類似于異常(但功能更強大)。例如,可以在把Animal對象添加到Animals集合中時,執行特定的代碼,而這部分代碼不是Animals類的一部分,也不是調用Add()方法的代碼的一部分。為此,需要給代碼添加事件處理程序,這是一種特殊類型的函數,在事件發生時調用。還需要配置這個處理程序,以監聽自己感興趣的事件。
使用事件可以創建事件驅動的應用程序,此類應用程序比讀者此時所能想到的多得多。例如,許多Windows應用程序完全依賴于事件。每個按鈕單擊或滾動條拖動操作都是通過事件處理實現的,其中事件是通過鼠標或鍵盤觸發的。
本章后面將介紹Windows應用程序中事件的工作原理,第13章將深入討論事件。
8.2.7 引用類型和值類型
在C#中,數據根據變量的類型以兩種方式中的一種存儲在一個變量中。變量的類型分為兩種:引用類型和值類型,其區別如下:
● 值類型在內存的同一處存儲它們自己和它們的內容。
● 引用類型存儲指向內存中其他某個位置(稱為堆)的引用,實際內容存儲在這個位置。
實際上,在使用C#時,不必過多地考慮這個問題。到目前為止,所使用的string變量(這是引用類型)與使用其他簡單變量(大多數是值類型,例如int)的方式完全相同。
值類型和引用類型的一個主要區別是:值類型總是包含一個值,而引用類型可以是null,表示它們不包含值。但是,可使用可空類型創建值類型,使值類型在這個方面的行為方式類似于引用類型(即可以為null)。第12章在介紹泛型(包括可空類型)這一高級主題時將討論這方面的內容。
只有string和object類型是簡單的引用類型。數組也是隱式的引用類型。我們創建的每個類都是引用類型,這就是在這里說明這一點的原因。
- C++面向對象程序設計(第三版)
- Java EE 6 企業級應用開發教程
- 摩登創客:與智能手機和平板電腦共舞
- AngularJS Web Application Development Blueprints
- Flask Web開發入門、進階與實戰
- INSTANT MinGW Starter
- Learning Python by Building Games
- Java程序設計
- Windows Phone 7.5:Building Location-aware Applications
- 批調度與網絡問題的組合算法
- 領域驅動設計:軟件核心復雜性應對之道(修訂版)
- Windows Embedded CE 6.0程序設計實戰
- GameMaker Essentials
- Python機器學習之金融風險管理
- 人人都能開發RPA機器人:UiPath從入門到實戰