360 likes | 439 Views
第九章 類別與物件. 9-1 物件導向的程式設計 9-2 物件的設計 9-3 物件的進階觀念 9-4 錯誤處理類別. 9-1 物件導向的程式設計. 人類之所以會是萬物之靈,其中一個主要原因是人類可以在錯誤中成長。物件導向的程式設計( Object Oriented Programming , OOP )也是人們在程式語言中逐漸累積的成果,這個觀念在 1970 年代就已提出,只是當時時機未到。現在, OOP 則已是所有程式語言的標準配備,為了說明 OOP 大行其道的原因,筆者將程式語言的發展分為 3 個時期,分別是非程序導向、程序導向及物件導向,說明如下:.
E N D
第九章 類別與物件 • 9-1 物件導向的程式設計 • 9-2 物件的設計 • 9-3 物件的進階觀念 • 9-4 錯誤處理類別
9-1 物件導向的程式設計 • 人類之所以會是萬物之靈,其中一個主要原因是人類可以在錯誤中成長。物件導向的程式設計(Object Oriented Programming,OOP)也是人們在程式語言中逐漸累積的成果,這個觀念在1970年代就已提出,只是當時時機未到。現在,OOP則已是所有程式語言的標準配備,為了說明OOP大行其道的原因,筆者將程式語言的發展分為3個時期,分別是非程序導向、程序導向及物件導向,說明如下:
非程序導向 • 早期的程式語言,並沒有內儲副程式(又稱程式庫)。所以,當我們開發新的應用程式時,如果某一功能與之前寫過的程式相近,則會將此段已完成的程式整段複製,並稍加修改即可重新加以利用。但是,這些程式的分身包括本尊,自從複製出來以後就開始以各自的方式發展,結果造成各版本的差異越來越大,這些程式很難弄清楚誰複製誰,彼此之間也難再共用某些程式碼,當遇到錯誤,或欲新增功能時,更是很難逐一修改所有的程式。
程序導向 • 為了解決以上程式共用問題,各編譯器廠商便開始提供一些大家常用的函式,比較有規模的軟體設計公司亦會將一些常用的函式集中在一個函式庫,旗下的軟體產品一律呼叫這些標準的函式庫,而不是從函式庫複製出來修改,此即為程序導向的程式設計 • 程序導向與非程序導向相比,的確解決了程式共用的問題,但是,人們並不以此為自滿,有些問題還是不夠順暢。例如,有些函式庫會隨著人們需求的增加而有不同版本,當某些函式功能增加時,我們只好重新取函式庫的函式加以修改,並賦與新的名稱。如此日積月累,我們的函式庫已有許多函式。這些函式有的功能相近、有的是前後版本不同、有的函式裏的變數來龍去脈不明,造成使用者的混亂。為了突破以上瓶頸,於是有物件導向的發展,以解決以上程序導向的不足。
物件導向 • 程序導向中的函式,存有許多解決問題的函式(函式在目前的物件導向中另稱為方法),它是偏重在方法的解決。但是,人類的生活方式不僅是單純的行為描述,更存在著屬性的記載。例如,當我們描述一個人時,我們通常描述如下:“他的名字是洪國勝,身高172、體重70,具有滾進、游泳及跑步的能力”;又例如描述一輛車子時,其描述如下:“它的名字是SENTRA,排氣量是1600 c.c.,耗油量是每公里0.1公升,且具有每小時120公里的移動能力”。以上人與車即稱為“物件”,名字、身高、排氣量則稱為“屬性”,而滾進、游泳、跑步、移動則稱為“方法”。既然真實世界是以物件描述物種,程式設計亦不應侷限在狹隘的函式,僅偏重解決問題的方法,而是應以物件的宏觀角度撰寫程式。所以,基於物件導向的新觀念,程式開發工具即製訂一種新的型別,此型別稱為類別。
每一類別都有屬於自己的方法,也就是我們已將眾多的函式依照類別存放,如此可解決目前日益龐大的函式命名與函式取用的困擾,就如同自然界是以界、門、綱、目、科、屬及種等作為物種的分類。例如,程序導向的時代,關於開門的函式即有電梯開門、汽車開門、房子開門等數種開門的方法,如此徒增命名與取每一類別都有屬於自己的方法,也就是我們已將眾多的函式依照類別存放,如此可解決目前日益龐大的函式命名與函式取用的困擾,就如同自然界是以界、門、綱、目、科、屬及種等作為物種的分類。例如,程序導向的時代,關於開門的函式即有電梯開門、汽車開門、房子開門等數種開門的方法,如此徒增命名與取 用的困擾。但在物件導向的領域裏,開門這個方法是附在相對應的類別裏。例如,於電梯類別裏有電梯的開門,汽車類別有汽車的開門,房子類別裏有房子的開門方法,大家的方法名稱都叫“開門”,撰寫程式時也是電梯.開門,汽車.開門,或是房子.開門(物件與方法、屬性之間以點(.)運算子連結),如此既可簡化程式的撰寫,亦可減少程式出錯的機會。但是,在程序導向的領域裏,所有的函式都集中,就有可能用錯方法。例如,用開電梯門的方法去開汽車的門,結果當然是錯的。 • 類別的變數即稱為物件,物件亦稱為類別的實現或類別的樣例化(Instance)。也就是說,類別就像是一個酒瓶或機器人的模型,在這些模型裏面已設計好他所具備的屬性與方法,當您需要酒瓶或機器人時,只要依照這些模型樣例一個或多個酒瓶或機器人即可。 • 其次,物件導向亦提出了三個觀念,分別是物件的封裝(Encapsulation)、繼承(Inheritance)及多型(Polymorphism),以解決程序導向的不足。
封裝 • 軟體科技的開發與其它的工業,如汽車、電視、收音機等機械、電子產品相較,可說是起步較晚的領域,在汽車、電視及收音機等產品上,我們發現這些產品很重視物件的封裝。例如,以電視機而言,我們發現內部有許多零件與開關,對於電視製造商而言,它們使用機殼將這些零件與開關“封裝”起來,才能避免使用者任意破壞,只留下部份開關與螢幕讓使用者欣賞節目。軟體程式的開發何嘗不應如此呢?所以對於類別的規劃,我們亦應重視所有方法、欄位及屬性封裝,使得這些成員有不同的封裝等級,以避免主程式與類別庫之間的干擾。就如同電視機或汽車的一些開關與旋鈕,有些是開放給一般的使用者調整,有的是讓維修工程師調整,有些則永遠不讓任何人調整。
繼承 • 任何新產品的開發,均不是無中生有,而是從舊有的產品中繼承某些特性,再加入新的零件或修改部份零件而成一項新的產品。例如,SENTRA 180正是繼承NEW SENTRA而來,只是排氣量提高了、內裝豪華了,但是原來的輪胎、方向盤及座椅還是用原來NEW SENTRA的東西,這就是繼承的道理,使得新產品的開發得以縮短時程。軟體的開發何嘗不應如此?繼承的另一優點是同一方法得以讓數個新舊版本同時存在。因為當你有新產品時,你不可能同時讓你的新舊客戶同時更新,所以您必須讓這些不同的版本同時存在,以滿足不同的產品需求。
多型 • 多型(Polymorphism)有些書譯成同名譯式,它的原文是希臘文,意思是說,一種樣式有多種表現的方式。例如,你有一個僕人專門幫你開門,那麼不論這門是內推,外拉或向旁邊推,你都是下同一指令“開門”,然後你的僕人即會依照門的結構而完成開門的動作。物件導向的程式設計亦發揚此理念,讓程式設計師於程式設計階段使用相同的指令,而編譯器能於執行階段依據不同的需求,執行不同的程式片段,此即為多型。Delphi物件導向的多型表現,分別是多載(Overloading)、改寫(Overriding)及抽象(Abstract)。
9-2 物件的設計 • 類別的定義 要設計一個物件,首要工作就是定義一種型態,此種型態稱為類別,例如,以下程式片段是我們之前常看到的TForm1類別,此一類別繼承自TForm1類別,其成員有6個欄位(分別是btnStart、Edit1、lblOut、Label1、Label2、btnClose)及一個方法(btnStartClick)。 type TForm1 = class(TForm) btnStart: TButton; Edit1: TEdit; lblOut: TLabel; Label1: TLabel; Label2: TLabel; btnClose: TButton; procedure btnStartClick(Sender: TObject); private { Private declarations } public { Public declarations } end;
以下則是筆者以類別重作範例4-2a 的結果,它的類別名稱是TPass ,繼承自TObject。 type TPass =class (Tobject) private Fa:integer; Fb:String; public procedure dispose(); property data :integer write Fa; property result:String read Fb; end; • TPass TPass 在此是類別名稱。依Delphi慣例,類別名稱均以大寫T開頭。 • class class是一個保留字,用於指定類別的繼承者。 • TObject TObject 是Delphi的類別中,最原始的類別,亦是Delphi預設的類別,所以此處的TObject亦可以省略,如下: TPass = class • private private 和 public是Delphi類別或員的封裝等級,關於類別成員的封裝等級,請看9-3節 • Fa、Fb Fa、Fb是TPass內部的欄位成員,其封裝等級是private,表示僅供類別內部存取。其次,依Delphi 的慣例,欄位成員通常以大寫F開頭。
procedure procedure是保留字,接在procedure後面的識別字,即為類別的方法成員,所以dispose即是TPass的方法,其封裝等級是public,表示可供類別外部存取。 • property property是保留字,接在property後面的識別字,即為類別的屬性。屬性的用途是類別與外界溝通的窗口。也就是透過屬性的設定,即可設定類別的欄位成員。本例的 property data:integer write Fa ; 即表示Fa是唯寫的屬性,類別內部可透屬性data去外界讀取資料,並寫入欄位Fa。另外本例 property result : String read Fb ; 則表示result屬性是唯讀的,類別外部可透過result屬性讀取類別內的Fb欄位值,再輸出。又例如 property result : String read Fb write Fb; • 則屬性result是可讀可寫的屬性。
方法的實作 • 前面的dispose是類別的方法成員,我們在類別內僅宣告其型態,其內部程式是在implementation(什麼是implementation?請看第十四章)中實作。實作的方式是以procedure或function保留字開頭,並加上型態名稱與方法名稱,且型態名稱與方法名稱中間是以點 (.)運算子隔開。例如,本例的dispose() 函式定義如下,函式或程序的實作與上一章相同。 Implementation {$R *.dfm} procedure TPass.dispose(); begin Fb:='不及格'; if (Fa>=60) then Fb:='及格'; end;
物件的樣例 • 之前TForm1類別的樣例程式如下: var Form1 : TForm1 ; • 同樣的道理,我們要將類別TPass樣例,其程式除了要宣告類別變數如下: var pa TPass; • 更要在使用物件之前,先建立物件,如下所示: pa:=Tpass.Create ; • 上式的類別變數pa,即稱為物件。
成員的存取 • 類別經過樣例之後,即可透過物件存取類別成員,其存取方式是物件名稱與成員名稱之間加上點運算子(.)。例如,以下敘述即可將score變數的值交由data屬性。 pa.data :=score ; • 以下敘述可執行dispose()方法: pa.dispose() ; • 以下敘述可將result屬性交由grade變數: grade : = pa.result ;
範例9-2a • 請將範例 4-2a,以物件重作。 程式說明: • 本例將TPass類別與TForm1類別放在同一單元,結果就是TPass類別是TForm1類別的子類別,所以無法表現類別成員的封裝特性。如下圖,Fa,Fb欄位原是不公開的成員,但卻都公開了。為了解決以上問題,TPass類別應放在一個獨立的單元(unit),請看以下範例。對於專案不了解的讀者,請先看第十四章的專案。
範例9-2b • 同上範例,但將類別TPass放在獨立的單元。 程式說明: • 本例已經有成員封裝的效果
範例9-2c • 同上範例,但增加存取屬性的方法。
範例9-2d 請以物件重作範例4-4a。 • 自我練習 請將本範例加上存取屬性的方法。
9-3 物件的進階觀念 • 封裝的等級 類別成員的封裝等級共有4種,分別是private、protected 、public及published,分別說明如下。 • private private是類別的私有成員,只有類別內部可使用該成員,類別外即無法存取這些成員 • protected protected是類別的保護成員,只有類別內部和繼承該類別的衍生類別可存取。 • public public 是類別的公開成員,類別內外皆可存取。 • published published的存取範圍與public相同,但其差別是published成員可產生執行時期的資訊(Running Type Information,RTTI),RTTI允許應用程式動態的查詢物件的欄位、屬性或支配物件的方法。當資料存檔或取檔時,RTTI可以在物件檢視視窗(Object Inspector)顯示物件的屬性或配合物件的方法顯示特殊屬性,請看範例23-1a。
類別的繼承 • 工業產品的開發,都是繼承原有產品的優良部份,再加入新的零件,物件導向的程式開發亦具有此繼承的特性,請看以下範例說明。
範例9-3a 同範例 9-2a,但新增判斷優等的方法.。 程式說明: • 本例物件Fa是Tpass2的樣例,而Tpass2繼承TPass,所以pa可存取TPass及Tpass2的所有公開的方法與屬性
方法的種類 方法可以是static、 virtual 及dynamic,其預設值是static,只有virtual及dynamic可以使用改寫(override)和抽象(abstract),但所有的方法都可以表現方法的多載(overloading)。 • virtual 和dynamic virtual 和dynamic在語意上是相同的,其差別是virtual 可得較快的執行速度,dynamic可得較小的程式碼,但都可表現方法的改寫。 • 改寫(override) 於基礎類別中,若有某些方法已不合時宜,此時即可於衍生類別中改寫此一方法。其方式為使用基礎類別相同的方法名稱與參數。且加上管制字override。例如,以下敘述,表示即將改寫dispose方法。 procedure dispose(); override; • 其次,欲改寫的方法,亦必於基礎類別中加上管制字virtual。例如,以下敘述,表示允許dispose方法於衍生類別中修改。 procedure dispose(); virtual;
範例9-3b • 示範方法的改寫。
抽象(Abstract)類別 • 於物件的繼承中,若某些方法(Method)於衍生類別需要有不同的處理方式,或根本不知後代的衍生類別如何去實作此方法,則可先將某些方法先行加上abstract,且方法內不用有任何實作,待其衍生類別再依所需自行實作。 其次,若某一類別內有一方法宣告為abstract,則該類別即為抽象類別,且該類別不能被樣例化,請看以下範例說明。
範例9-3c • 示範抽象類別。
多載(Overloading) • 我們在第八章已介紹函式與程序的多載,類別中的方法亦是由函式與程序組合而成,故亦適用多載,以下範例即以方法的多載重作範例 8-5a。
範例9-3d • 示範方法的多載。
建構子(Contrcutors) 建構子是一個較特殊的方法,它會在類別樣例時自動執行。建構子宣告的方式為在Create之前加上保留字constructor,如下所示: constructor Ceate ; • Create方法的實作,亦是比照一般的方法,只是將procedurde或function置換成constructor。如下所示,是在建構子中預設欄位Fa的初值。 constructor TPass.Create ; begin Fa:=33; end;
範例9-3e • 示範建構子
解構子(Destructors_) 前面的建構子是類別樣例化時自動執行的方法,相對於建構子亦有所謂的解構子。所謂的解構子則是物件被釋放前自動執行的方法,其內容可放一些釋放物件時所要執行的敘述。解構子的用法完全與建構子相同,如下所示是解構子的宣告。 destructor Destroy ; destructor Destroy ;override; • 解構子的內容亦同建構子的撰寫方式,如下所示 destructor TPass.Destroy ; begin Fb:='再見'; //請將釋放物件所要執行的敘述放於此 end;
9-4 錯誤處理類別 若干執行時期的錯誤並非程式設計師的錯誤,而是使用者的輸入錯誤與環境所造成。例如,除數為0、磁碟已滿等問題,若系統因此而停頓,則會造成客戶的抱怨。 • try...except 為了避免使用者的不慎或思慮欠週所發生的錯鋘,程式設計者可於可能出錯的地方,以try..except捕捉錯誤發生,並給予適當的處理,以避免系統當機。請以下範例說明。
範例9-4a • 示範try....except的使用,本例將範例 3-4a的除法運算,加上一些保護措施。
raise 程式設計時,程式設計師通常很難一一產生所有的錯誤,此時就無法測試錯誤處理程式是否有效。為了提供錯誤的來源,Delphi提供raise保留字以便讓程式設計師能摸擬錯誤的來源,就如消防演習一樣,指揮官說三樓著火,大家就跟隨演練滅火與逃生,以便測試消防員及住戶的應變能力,請看以下範例說明。
範例9-4b • 示範raise的使用。
習題 • 1.請將範例4-4b,求解二元一次方程式,以類別完成。 • 2.請將範例6-1g,以類別的方式重作。