- C#入門經典(第7版):C# 6.0 & Visual Studio 2015(.NET開發經典名著)
- (美)Beijamin Perkins Jacob Vibe Hammer Jon D. Reid
- 5119字
- 2021-04-02 21:18:42
8.1 面向對象編程的含義
面向對象編程解決了傳統編程技巧的許多問題。前面介紹的編程方法稱為函數(或過程)化編程,常會導致所謂的單一應用程序,即所有功能都包含在幾個代碼模塊(常常是一個代碼模塊)中。而使用OOP技術,常常要使用許多代碼模塊,每個模塊都提供特定功能。而且,每個模塊都是孤立的,甚至與其他模塊完全獨立。這種模塊化編程方法提供了非常大的多樣性,大大增加了重用代碼的機會。
為進一步說明這個問題,把計算機上的一個高性能應用程序想象成一輛一流賽車。如果使用傳統的編程技巧,這輛賽車就是一個單元。如果要改進這輛車,就必須替換整車,把它送回廠商那里,讓汽車專家升級它,或者購買一輛新車。如果使用OOP技術,就只需要從廠商處購買新的引擎,自己按照其說明替換它,而不必用鋼鋸切割車體。
在傳統應用程序中,執行流常是簡單的、線性的。把應用程序加載到內存中,從A點開始執行,在B點結束,然后從內存中卸載,在這個過程中可能用到其他各種實體,例如存儲介質上的文件或顯卡的功能,但處理的主體總是位于一個地方。用到的代碼一般與使用各種數學和邏輯方式處理數據相關。處理方法通常比較簡單,使用基本的數據類型(例如整型和布爾值)建立比較復雜的數據表達方式。
而使用OOP,事情就不是這么直接了。盡管可以獲得相同的效果,但其實現方式是完全不同的。OOP技術以結構、數據的含義以及數據和數據之間的交互操作為基礎。這通常意味著要把更多精力放在項目的設計階段,其好處是項目的可擴展性比較高。一旦對某種類型的數據的表達方式達成一致,這種表達方式就會應用到應用程序以后的版本中,甚至是全新應用程序中。這種一致的表達方式可以極大地縮短開發時間。這就是上述賽車示例的工作原理。這里的一致是指“引擎”的代碼是結構化的,這樣就可以很容易地替換成新代碼(即新引擎),而不需要找廠商幫忙。這也表示,引擎創建出來后可用于其他目的,可以把它安裝到另一輛車上,或者用它驅動潛艇。
除了數據表達方式的一致性外,OOP編程還常可以簡化任務,因為較抽象實體的結構和用法也是一致的。例如,不僅把輸出結果發送給設備(如打印機)所使用的數據格式是一致的,而且與該設備交換數據的方法也是一致的,這包括它理解的指令等。回到賽車示例上,要達成的一致做法包括引擎如何連接到油箱,如何把驅動力傳送給車輪等。
顧名思義,OOP技術要使用對象。
8.1.1 對象的含義
對象就是OOP應用程序的一個組成部件。這個組成部件封裝了部分應用程序,這部分程序可以是一個過程、一些數據或一些更抽象的實體。
簡單地說,對象非常類似于本書前面討論的結構類型,包含變量成員和函數類型。它所包含的變量組成了存儲在對象中的數據,其中包含的函數可以訪問對象的功能。略為復雜的對象可能不包含任何數據,而只包含函數,表示一個過程。例如,可以使用表示打印機的對象,其中的函數可以控制打印機(允許打印文檔、測試頁等)。
C#中的對象是從類型中創建的,就像前面的變量一樣。對象的類型在OOP中有一個特殊名稱:類。可以使用類的定義實例化對象,這表示創建該類的一個命名實例。“類的實例”和對象的含義相同,但“類”和“對象”是完全不同的概念。
注意:術語“類”和“對象”常常混淆,從一開始就正確區分它們是非常重要的,使用前面的賽車示例有助于區分這兩個術語。在這個示例中,類是指汽車的模板,或者用于構建汽車的規劃。汽車本身是這些規劃的實例,所以可以看成對象。
本章將使用統一建模語言(Unified Modeling Language, UML)語法研究類和對象。UML是為應用程序建模而設計的,從組成應用程序的對象,到它們執行的操作,到我們希望有的用例,應有盡有。這里只使用這門語言的基本部分,在使用它們的過程中進行解釋,但不考慮比較復雜的部分,因為UML是一個很專業的主題,有很多圖書專門介紹它。
圖8-1是打印機類Printer的UML表示方法。類名顯示在這個框的頂部(后面將論述下面兩個區域)。

圖8-1
圖8-2是這個Printer類的一個實例myPrinter的UML表示方法。

圖8-2
在頂部,首先顯示實例名,其后是類名。這兩個名稱用一個冒號分隔。
1.屬性和字段
可以通過屬性和字段訪問對象中包含的數據。這個對象數據可以用于區分不同的對象,因為同一個類的不同對象在屬性和字段中存儲了不同的值。
包含在對象中的不同數據構成了對象的狀態。假定一個對象類表示一杯咖啡,稱為CupOfCoffee。在實例化這個類(即創建這個類的對象)時,必須提供對類有意義的狀態。此時可以使用屬性和字段,讓代碼能通過該對象設置要使用的咖啡品牌,咖啡中是否加牛奶或方糖,咖啡是否即溶等。于是,給定的這杯咖啡對象就有了指定的狀態,例如,加牛奶和兩塊方糖的哥倫比亞過濾咖啡。
字段和屬性都可以鍵入,所以可以把信息存儲在字段和屬性中,作為string值、int值等。但屬性與字段是不同的,因為屬性不提供對數據的直接訪問。對象能讓用戶不考慮數據的細節,不需要在屬性中用一對一的方式表示。如果在CupOfCoffee實例中使用一個字段表示方糖的數量,用戶就可以在該字段中放置自己喜歡的值,其取值范圍僅由存儲該信息的類型來限制。例如,如果使用int來存儲這個數據,用戶就可以使用-2147483648至2147483647之間的任意值,如第3章所述。顯然,并不是所有的值都有意義,尤其是負值,一些較大的正值將需要非常大的咖啡杯。但如果使用一個屬性來表示,就可以限制這個值,例如介于0和2之間的一個數字。
一般情況下,在訪問狀態時最好提供屬性而不是字段,因為這樣可以更好地控制各種行為,這個選擇不會影響使用對象實例的代碼,因為使用屬性和字段的語法是相同的。
對屬性的讀寫訪問也可以由對象來明確定義。某些屬性是只讀的,只能查看它們的值,而不能改變它們(至少不能直接改變)。這常常是同時讀取幾個狀態的一個有效技巧。CupOfCoffee類有一個只讀屬性Description,在請求它時,就返回一個字符串,表示該類的一個實例的狀態(例如前面給出的字符串)。也可以通過查看幾個屬性,把相同的數據組合起來,但這樣的屬性可以節省時間和精力。還可以有只寫的屬性,其操作方式是類似的。
除了對屬性的讀/寫訪問外,還可以為字段和屬性指定另一種訪問權限,稱為可訪問性。可訪問性確定了什么代碼可以訪問這些成員,它們可用于所有代碼(公共)還是只能用于類中的代碼(私有),或者使用更復雜的模式(詳見本章后面的內容)。常見的情況是把字段設置為私有,通過公共屬性訪問它們。這樣,類中的代碼就可以直接訪問存儲在字段中的數據,而公共屬性禁止外部用戶訪問這些數據,以防他們在其中放置無效的內容。公共成員是類公開的成員。
要更清晰地闡明這個問題,可以把可訪問性與變量的作用域等同起來。例如,私有字段和屬性可以看成擁有它們的對象的局部成員,而公共字段和屬性的作用域也包括對象以外的代碼。
在類的UML表示方法中,用第二部分顯示屬性和字段,如圖8-3所示。

圖8-3
這是CupOfCoffee類的表示方式,前面為它定義了5個成員(屬性或字段,在UML中,它們沒有區別)。每個成員都包含下述信息:
● 可訪問性:+號表示公共成員,-號表示私有成員。但一般情況下,本章的圖中不顯示私有成員,因為這些信息是類內部的信息。至于讀/寫訪問,則不提供任何信息。
● 成員名。
● 成員的類型。
冒號用于分隔成員名和類型。
2.方法
“方法”這個術語用于表示對象中的函數。這些函數調用的方式與其他函數相同,使用返回值和參數的方式也相同(詳見第6章)。
方法用于訪問對象的功能。與字段和屬性一樣,方法也可以是公共的或私有的,按照需要限制外部代碼的訪問。它們通常使用對象的狀態來影響它們的操作,在需要時訪問私有成員,如私有字段。例如,CupOfCoffee類定義了一個方法AddSugar(),該方法對遞增方糖數提供了比設置相應的Sugar屬性更易讀的語法。
在UML的類框中,方法顯示在第三部分,如圖8-4所示。

圖8-4
其語法類似于字段和屬性,但最后顯示的類型是返回類型,在這一部分,還顯示了方法的參數。在UML中,每個參數都帶有下述標識符之一:return、in、out或inout。它們用于表示數據流的方向,其中out和inout大致對應于第6章討論的C#關鍵字out和ref。in大致對應于C#中不使用這兩個關鍵字的情形(默認情形)。return表示傳回調用方法的值。
8.1.2 一切皆對象
本書一直在使用對象、屬性和方法。實際上,C#和.NET Framework中的所有東西都是對象。控制臺應用程序中的Main()函數就是類的一個方法。前面介紹的每個變量類型都是一個類。前面使用的每個命令都是屬性或方法,例如<String>.Length和<String>.ToUpper()等。句點字符把對象實例名與屬性或方法名分隔開來,方法名后面的()把方法與屬性區分開來。
對象無處不在,使用它們的語法通常比較簡單,至少到現在為止都足夠簡單,使我們可以集中精力討論C#中一些比較基礎的方面。從現在開始詳細介紹對象。這里討論的概念都具有深遠影響,它們甚至適用于簡單的int變量。
8.1.3 對象的生命周期
每個對象都有一個明確定義的生命周期,除了“正在使用”的正常狀態之外,還有兩個重要的階段:
● 構造階段:第一次實例化一個對象時,需要初始化該對象。這個初始化過程稱為構造階段,由構造函數完成。
● 析構階段:在刪除一個對象時,常常需要執行一些清理工作,例如釋放內存,這由析構函數完成。
1.構造函數
對象的初始化過程是自動完成的。我們不需要自己尋找適于存儲新對象的內存空間。但是,在初始化對象的過程中,有時需要執行一些額外工作。例如,需要初始化對象存儲的數據。構造函數就是用于初始化數據的函數。
所有的類定義都至少包含一個構造函數。在這些構造函數中,可能有一個默認構造函數,該函數沒有參數,與類同名。類定義還可能包含幾個帶有參數的構造函數,稱為非默認的構造函數。代碼可以使用它們以許多方式實例化對象,例如給存儲在對象中的數據提供初始值。
在C#中,用new關鍵字來調用構造函數。例如,可用下面的方式通過其默認的構造函數實例化一個CupOfCoffee對象:
CupOfCoffee myCup = new CupOfCoffee();
還可以用非默認的構造函數來實例化對象。例如,CupOfCoffee類有一個非默認的構造函數,它使用一個參數在初始化時設置咖啡豆的品牌:
CupOfCoffee myCup = new CupOfCoffee("Blue Mountain");
構造函數與字段、屬性和方法一樣,可以是公共或私有的。在類外部的代碼不能使用私有構造函數實例化對象,而必須使用公共構造函數。這樣,通過把默認構造函數設置為私有的,就可以強制類的用戶使用非默認的構造函數。
一些類沒有公共的構造函數,外部的代碼就不可能實例化它們,這些類稱為不可創建的類,但如稍后所述,這些類并不是完全沒有用的。
2.析構函數
.NET Framework使用析構函數來清理對象。一般情況下,不需要提供析構函數的代碼,而由默認的析構函數自動執行操作。但是,如果在刪除對象實例前需要完成一些重要操作,就應提供具體的析構函數。
例如,如果變量超出了范圍,代碼就不能訪問它,但該變量仍存在于計算機內存的某個地方。只有在.NET運行程序執行其垃圾回收,進行清理時,該實例才被徹底刪除。
8.1.4 靜態成員和實例類成員
屬性、方法和字段等成員是對象實例所特有的,此外,還有靜態成員(也稱為共享成員,尤其是Visual Basic用戶常使用這個術語),例如靜態方法、靜態屬性或靜態字段。靜態成員可以在類的實例之間共享,所以可以將它們看成類的全局對象。靜態屬性和靜態字段可以訪問獨立于任何對象實例的數據,靜態方法可以執行與對象類型相關但與對象實例無關的命令。在使用靜態成員時,甚至不需要實例化對象。
例如,前面使用的Console.WriteLine()和Convert.ToString()方法就是靜態的,根本不需要實例化Console或Convert類(如果試著進行這樣的實例化,操作會失敗,因為這些類的構造函數不是可公共訪問的,如前所述)。
許多情況下,靜態屬性和靜態方法有很好的效果。例如,可以使用靜態屬性跟蹤給類創建了多少個實例。在UML語法中,類的靜態成員帶有下劃線,如圖8-5所示。

圖8-5
1.靜態構造函數
使用類中的靜態成員時,需要預先初始化這些成員。在聲明時,可以給靜態成員提供一個初始值,但有時需要執行更復雜的初始化操作,或者在賦值、執行靜態方法之前執行某些操作。
使用靜態構造函數可以執行此類初始化任務。一個類只能有一個靜態構造函數,該構造函數不能有訪問修飾符,也不能帶任何參數。靜態構造函數不能直接調用,只能在下述情況下執行:
● 創建包含靜態構造函數的類實例時
● 訪問包含靜態構造函數的類的靜態成員時
在這兩種情況下,會首先調用靜態構造函數,之后實例化類或訪問靜態成員。無論創建了多少個類實例,其靜態構造函數都只調用一次。為了區分靜態構造函數和本章前面介紹的構造函數,也將所有非靜態構造函數稱為實例構造函數。
2.靜態類
我們常常希望類只包含靜態成員,且不能用于實例化對象(如Console)。為此,一種簡單的方法是使用靜態類,而不是把類的構造函數設置為私有。靜態類只能包含靜態成員,不能包含實例構造函數,因為按照定義,它根本不能實例化。但靜態類可以有一個靜態構造函數,如上一節所述。
注意:如果以前完全沒有接觸過OOP,在閱讀本章的其他內容之前,應該停下來將OOP研究一番。在學習更復雜的OOP內容之前,全面掌握基礎知識是很重要的。