Delphi 之 第四講 自定義資料型別
為什麼要使用自定義資料型別呢?原因很簡單,在現實生活中,比如一個學生他有姓名,學號,年齡,身高,出生年月,而在上一講中,我們提到的資料型別無法滿足現實生活需求,這個時候,自定義資料型別該派上用場了,我們可以定一個學生型別。從而達到我們的需求了。自定義資料型別又可以分為子界型別,陣列型別,記錄型別,列舉型別,指標型別,集合型別等等。。。
下面分別對這些資料型別講解
子界型別
子界型別定義了某種型別的取值範圍(因此定名subrange)。你可定義整數型別的子界型別,如取值從1到10或從100到1000,或者定義字元 型別的子界型別,如下所示:
type Ten = 1..10; OverHundred = 100..1000; Uppercase = 'A'..'Z';
定義子界型別時,你不需要指定基類的名字,而只需提供該型別的兩個常數。所用基類必須是有序型別,定義結果將是另一種有序型別。
如定義一個子界變數,那麼賦給該變數的值必須是子界定義範圍內的值。下面程式碼是正確的:
var UppLetter: UpperCase; begin UppLetter := 'F';
以下程式碼則是不正確的:
var UppLetter: UpperCase; begin UppLetter := 'e'; // compile-time error
以上程式碼將導致一個編譯錯誤:“Constant expression violates subrange bounds
如果代之以下面程式碼:
var UppLetter: Uppercase; Letter: Char; begin Letter :='e'; UppLetter := Letter;
Delphi 編譯會通過,但在執行時,如果你開啟了範圍檢查編譯選項(在工程選項對話方塊的編譯器頁設定),你將得到 Range check error (範圍檢測錯誤)資訊。
注意:建議你在開發程式時開啟上述編譯選項,以使程式更健壯並易於除錯。這樣即使遇上錯誤,你也會得到一個明確的資訊而不是難以琢磨的行為。最終完成程式時你可以去掉這個選項,使程式執行得快一些,不過影響很小。因此我建議你開啟所有執行時的檢測選項,如溢位檢查和堆疊檢查,甚至提交程式時仍然保留它們。
列舉型別
列舉型別又是一種自定義有序型別。在列舉型別中,你列出所有該型別可能取的值,而不是指定現有型別的範圍。換句話說,列舉型別是個可取值的序列。見下例:
type Colors = (Red, Yellow, Green, Cyan, Blue, Violet); Suit = (Club, Diamond, Heart, Spade);
序列中每個值都對應一個序號,序號從0開始計數。使用Ord 函式,即可得到一個列舉型別值的序號。例如,Ord (Diamond) 返回值1。
注意:列舉型別有多種內部表示法。預設時,Delphi 用8位表示法;如果有多於256個不同的值,則用16位表示法。還有一種32位表示法,需要與C、C++庫相容時會用到。使用$Z 編譯指令可改變預設設定,請求更多位的表示法。
Delphi VCL(可視控制元件庫)在很多地方用了列舉型別。例如,窗體邊框型別定義如下:
type TFormBorderStyle = (bsNone, bsSingle, bsSizeable, bsDialog, bsSizeToolWin, bsToolWindow);
當屬性值是列舉型別時,你可以從Object Inspector顯示的下拉列表框中選值,如圖4.1所示。
圖 4.1 Object Inspector 中的列舉型別屬性
Delphi 幫助檔案中列出了各種Delphi VCL列舉型別的可能值。你也可以通過OrdType程式檢視Delphi 列舉型別、集合型別、子界型別及任何其他有序型別的取值列表。圖4.2為這個例子的輸出結果。
圖 4.2: 程式 OrdType 顯示的列舉型別詳細資訊
集合型別
集合型別表示一組值,該組值由集合所依據的有序型別定義。定義集合的常用有序型別不多,一般為列舉型別或子界型別。如果子界型別取值為1..3,那麼基於它的集合型別值可以是1、或2、或3、或1和2、或1和3、或2和3、或取所有3個數、或一個數也沒有。
一個變數通常包含該型別對應的一個值,而集合型別可以不包含值、包含一個值、兩個值、三個值,或更多,它甚至可以包含定義範圍內所有的值。下面定義一個集合:
type Letters = set of Uppercase;
現在我可以用上面型別來定義變數,並把原始型別的值賦給變數。為了在集合中表示一組值,需要用逗號將值隔開,最後用方括號結尾。下例顯示了多值、單值和空值的變數賦值:
var Letters1, Letters2, Letters3: Letters; begin Letters1 := ['A', 'B', 'C']; Letters2 := ['K']; Letters3 := [];
在Delphi中,集合一般用於表示有多種選擇的標記。例如下面兩行程式碼(摘自Delphi庫)聲明瞭一個列舉型別,其中列出了視窗條上可選的圖示,並聲明瞭相應的集合型別:
type TBorderIcon = (biSystemMenu, biMinimize, biMaximize, biHelp); TBorderIcons = set of TBorderIcon;
實際上,給定的視窗中可以沒有圖示,也可以有一個或多個圖示。用Object Inspector設定時(見圖4.3),雙擊屬性名,或單擊屬性左邊的加號,自行選擇,從而新增或刪除集合中的值。
另一個基於集合型別的屬性是字型。字型型別值可以是粗體、斜體、帶下畫線、帶刪除線等,一種字型可以既是斜體又是粗體,也可以沒有屬性,或者帶有全部的屬性。因此用集合型別來表示它。你可以象下面程式碼那樣,在程式中給集合賦值:
Font.Style := []; // no style Font.Style := [fsBold]; // bold style only Font.Style := [fsBold, fsItalic]; // two styles
你也能對一個集合進行許多不同方式的操作,包括把兩個相同型別的集合變數相加(或更準確地說,計算兩個集合變數的並集):
Font.Style := OldStyle + [fsUnderline]; // two sets
此外,你可以通過OrdType 查閱Delphi 控制元件庫中定義的集合型別取值列表。OrdType 放在本書原始碼的TOOLS 目錄中。
陣列型別
陣列型別定義了一組指定型別的元素序列,在方括號中填入下標值就可訪問陣列中的元素。定義陣列時,方括號也用來指定可能的下標值。例如,下面的程式碼中定義了一個有24個整數的陣列:
type DayTemperatures = array [1..24] of Integer;
在陣列定義時,你需要在方括號中填入一個子界型別的值,或者用兩個有序型別的常量定義一個新的子界型別,子界型別指定了陣列的有效索引。由於子界型別指定了陣列下標值的上界和下界,那麼下標就不必象C、C++、JAVA和其他語言那樣必須從零開始。
由於陣列下標基於子界型別,因此Delphi 能夠對它們進行範圍檢查。不合法的常量子界型別將導致一個編譯時間錯誤;如果選上編譯器範圍檢查選項,那麼超出範圍的下標值將導致一個執行時間錯誤。
使用上述陣列定義方法,定義一個DayTemperatures 型別的變數如下:
type DayTemperatures = array [1..24] of Integer; var DayTemp1: DayTemperatures; procedure AssignTemp; begin DayTemp1 [1] := 54; DayTemp1 [2] := 52; ... DayTemp1 [24] := 66; DayTemp1 [25] := 67; // compile-time error
陣列可以是多維的,如下例:
type MonthTemps = array [1..24, 1..31] of Integer; YearTemps = array [1..24, 1..31, Jan..Dec] of Integer;
這兩個陣列建立在相同的核心型別上,因此你可用前面定義的資料型別宣告它們,如下面程式碼所示:
type MonthTemps = array [1..31] of DayTemperatures; YearTemps = array [Jan..Dec] of MonthTemps;
上例的宣告把索引的次序前後調換了一下,但仍允許變數之間整塊賦值。例如:把一月份的溫度值賦給二月份:
var ThisYear: YearTemps; begin ... ThisYear[Feb] := ThisYear[Jan];
你也能定義下標從零開始的陣列,不過這似乎不太合邏輯,因為你需要用下標2來訪問陣列第三項。然而,Windows一直沿用了從零開始的陣列(因為它是基於C語言的),並且Delphi 控制元件庫也在往這方向靠攏。
使用陣列時,你總要用標準函式Low和 High來檢測它的邊界,Low和 High返回下標的下界和上界。強烈建議使用Low和 High運算元組,特別是在迴圈中,因為這樣能使程式碼與陣列範圍無關,如果你改變陣列下標的範圍宣告,Low和 High程式碼不會受影響;否則,如果程式碼中有一個數組下標迴圈體,那麼當陣列大小改變時你就不得不更新迴圈體的程式碼。Low和 High將使你的程式碼更易於維護、更穩定。
注意:順便提一下,使用Low和 High不會增加系統執行額外開銷。因為在編譯時,他們已被轉換成常數表示式,而不是實際函式呼叫。其他簡單的系統函式也是這樣。
Delphi主要以陣列屬性的形式使用陣列。我們已經在 TimeNow 例子中看到過陣列屬性,也就是ListBox控制元件的Items 屬性。下一章討論Delphi迴圈時,我將向你介紹更多有關陣列屬性的例子。
注意:Delphi 4 的Object Pascal中增加了動態陣列,所謂動態陣列是在執行時動態分配記憶體改變陣列大小。使用動態陣列很容易,不過我認為在這裡討論這類陣列不合適。你將在第八章看到對Delphi 動態陣列的描述。
記錄型別
記錄型別用於定義不同型別資料項的固定集合。記錄中每個元素,或者說域,有它自己的型別。記錄型別定義中列出了所有域,每個域對應一個域名,通過域名可以訪問它。
下面簡單列舉了記錄型別的定義、型別變數的宣告以及這類變數的使用:
type Date = record Year: Integer; Month: Byte; Day: Byte; end; var BirthDay: Date; begin BirthDay.Year := 1997; BirthDay.Month := 2; BirthDay.Day := 14;
類和物件可以看作是記錄型別的擴充套件。Delphi 庫趨向於用類替代記錄型別,不過Windows API中定義了許多記錄型別。
記錄型別中允許包含variant 域,它表示多個域能公用同一記憶體區,而且域可以是不同型別(這相應於C語言中的聯合union)。換句話說,你可以通過variant 域或說是一組域訪問記錄中同一個記憶體位置,但是各個值仍需區別對待。variant型別主要用來存貯相似但又不同的資料,進行與型別對映(typecasting)相似的型別轉換(自從typecasting 引入Pascal,已很少用到這種方法了)。雖然Delphi在一些特殊情況下還在用variant 記錄型別,但是現在已經被面向物件技術或其他現代技術代替了。
variant 記錄型別的應用不符合型別安全原則,因此不提倡在程式設計中使用,初學者更是如此。實際上,專家級的程式設計人員確實需要用到variant 記錄型別,Delphi 庫的核心部分就用到了這一型別。不管怎樣,除非你是個Delphi 專家,否則你應避免使用variant記錄型別。
指標
指標是存放指定型別(或未定義型別)變數記憶體地址的變數,因此指標間接引用一個值。定義指標不需用特定的關鍵字,而用一個特殊字元,這個特殊字元是脫字元號(^),見下例:
type PointerToInt = ^Integer;
一旦你定義了指標變數,你就可以用@ 符號把另一個相同型別變數的地址賦給它。見下例:
var P: ^Integer; X: Integer; begin P := @X; // change the value in two different ways X := 10; P^ := 20;
如果定義了一個指標P,那麼P表示指標所指向的記憶體地址,而P^表示記憶體所儲存的實際內容。因此,在上面的程式碼中, P^ 與X相等。
除了表示已分配記憶體的地址外,指標還能通過New 例程在堆中動態分配記憶體,不過當你不需要這個指標時,你也必須呼叫Dispose 例程釋放你動態分配的記憶體。
var P: ^Integer; begin // initialization New (P); // operations P^ := 20; ShowMessage (IntToStr (P^)); // termination Dispose (P); end;
如果指標沒有值,你可以把nil 賦給它。這樣,你可以通過檢查指標是否為nil 判斷指標當前是否引用一個值。這經常會用到,因為訪問一個空指標的值會引起一個訪問衝突錯誤,也就是大家知道的“一般保護錯”(GPF)。見下例:
procedure TFormGPF.BtnGpfClick(Sender: TObject); var P: ^Integer; begin P := nil; ShowMessage (IntToStr (P^)); end;
通過執行例GPF,或者看圖4.4,你可以看到上述這種結果
將上面程式加以修改,訪問資料就安全了。現在將一個已存在的區域性變數賦給指標,指標使用就安全了,雖然如此,我還是加上了一個安全檢查語句:
procedure TFormGPF.BtnSafeClick(Sender: TObject); var P: ^Integer; X: Integer; begin P := @X; X := 100; if P <> nil then ShowMessage (IntToStr (P^)); end;
Delphi 還定義了一個Pointer 資料型別,它表示無型別的指標(就象C語言中的void* )。如果你使用無型別指標,你應該用GetMem 例程,而不是New例程,因為GetMem 例程能用於記憶體分配大小不確定的情況。
實際上,Delphi 中必須使用指標的情況很少,這是Delphi開發環境一個誘人的優點。雖然如此,若要進行高階程式設計和完全理解Delphi 物件模型,理解指標是很重要的,因為Delphi 物件模型在幕後使用了指標。
注意:雖然在Delphi中不常使用指標,但是你經常會用一個極為相似的結構--引用(references)。每個物件例項實際上是一個隱含的指標,或說是對其實際資料的引用,利用引用,你能象用其他資料型別一樣使用物件變數。
檔案型別
另一個Pascal特定的型別構造器是檔案型別(file)。檔案型別代表物理磁碟檔案,無疑是Pascal語言的一個特殊型別。按下面的方式,你可以定義一個新的資料型別:
type IntFile = file of Integer;
然後,你就能開啟一個與這個結構相應的物理檔案、向檔案中寫入整數、或者從檔案中讀取當前的值。
Pascal 檔案型別的使用很直觀,而且Delphi 中也定義了一些控制元件用於檔案儲存和裝載,以及對資料流和資料庫的支援。
以上內容比較多,但是比較簡單,上機動手試試吧!!!
結束語
我們討論了自定義資料型別,完成了對Pascal 資料型別體系的介紹,為下一部分“語句”作好了準備。語句用於操作我們所定義的變數。