我學Delphi心得及筆記—-過程與函數(第五講)

來源:互聯網
上載者:User

常式(routine)是Pascal 的一個重要概念,常式由一系列語句組成,常式名是唯一的,通過常式名你可以多次調用它,這樣程式中只需要一個常式就夠了,由此避免了代碼多次重複,而且代碼也容易修改維護。從這個角度看,你可以認為常式是一種基本的代碼封裝機制。介紹完Pascal 常式的文法後,我會回過頭來舉例說明這個問題。

Pascal 過程與函數

Pascal中的常式有兩種形式:過程和函數。理論上說,過程是你要求電腦執行的操作,函數是能傳回值的計算。兩者突出的不同點在於:函數能返回計算結果,即有一個傳回值,而過程沒有。兩種類型的常式都可以帶多個給定類型的參數。

不過實際上函數和過程差別不大,因為你可以調用函數完成一系列操作,跳過其傳回值(用可選的出錯代碼或類似的東西代替傳回值);也可以通過過程的參數傳遞計算結果(這種參數稱為引用,下一部分會講到)。

下例定義了一個過程、兩個函數,兩個函數的文法略有不同,結果是完全相同的。

procedure Hello;begin  ShowMessage ('Hello world!');end;function Double (Value: Integer) : Integer;begin  Double := Value * 2;end;// or, as an alternativefunction Double2 (Value: Integer) : Integer;begin  Result := Value * 2;end;

流行的做法是用Result 給函數賦傳回值,而不是用函數名,我認為這樣的代碼更易讀。

一旦定義了這些常式,你就可以多次調用,其中調用過程可執行操作;調用函數能計算傳回值。如下:

procedure TForm1.Button1Click (Sender: TObject);begin  Hello;end; procedure TForm1.Button2Click (Sender: TObject);var  X, Y: Integer;begin  X := Double (StrToInt (Edit1.Text));  Y := Double (X);  ShowMessage (IntToStr (Y));end;

注意:現在不必考慮上面兩個過程的文法,實際上它們是方法。只要把兩個按鈕(button)放到一個Delphi 表單上,在設計階段單擊它們,Delphi IDE將產生合適的支援代碼,你只需要填上begin 和end 之間的那幾行代碼就行。編譯上面的代碼,需要你在表單中加一個Edit控制項。

現在回到我前面提到過的代碼封裝概念。當你調用Double 函數時,你不需要知道該函數的具體實現方法。如果以後發現了更好的雙倍數計算方法,你只需要改變函數的代碼,而調用函數的代碼不必改變(儘管代碼執行速度可能會加快!)。Hello 過程也一樣,你可以通過改變這個過程的代碼,修改程式的輸出,Button2Click 方法會自動改變顯示結果。下面是改變後的代碼:

procedure Hello;begin  MessageDlg ('Hello world!', mtInformation, [mbOK],0);end;

提示:當調用一個現有的Delphi 函數、過程或任何VCL方法時,你應該記住參數的個數及其資料類型。不過,只要鍵入函數或過程名及左括弧,Delphi 編輯器中會出現即時提示條,列出函數或過程的參數表供參考。這一特性被稱為代碼參數(Code Parameters) ,是代碼識別技術的一部分。

引用參數

Pascal 常式的傳遞參數可以是值參也可以是引用參數。值參傳遞是預設的參數傳遞方式:即將值參的拷貝壓入棧中,常式使用、操縱的是棧中的拷貝值,不是原始值。

當通過引用傳遞參數時,沒有按正常方式把參數值的拷貝壓棧(避免拷貝值壓棧一般能加快程式執行速度),而是直接引用參數原始值,常式中的代碼也同樣訪問原始值,這樣就能在過程或函數中改變參數的值。引用參數用關鍵字var 標示。

參數引用技術在大多數程式設計語言中都有,C語言中雖沒有,但C++中引入了該技術。在C++中,用符號 &表示引用;在VB中,沒有ByVal 標示的參數都為引用。

下面是利用引用傳遞參數的例子,引用參數用var關鍵字標示:

procedure DoubleTheValue (var Value: Integer);begin  Value := Value * 2;end;

在這種情況下,參數既把一個值傳遞給過程,又把新值返回給調用過程的代碼。當你執行完以下代碼時:

var  X: Integer;begin  X := 10;  DoubleTheValue (X);

x變數的值變成了20,因為過程通過引用訪問了X的原始儲存單元,由此改變了X的初始值。

通過引用傳遞參數對有序類型、傳統字串類型及大型記錄類型才有意義。實際上Delphi總是通過值來傳遞對象,因為Delphi對象本身就是引用。因此通過引用傳遞對象就沒什麼意義(除了極特殊的情況),因為這樣相當於傳遞一個引用到另一個引用。

Delphi 長字串的情況略有不同,長字串看起來象引用,但是如果你改變了該字串的串變數,那麼這個串在更新前將被拷貝下來。作為值參被傳遞的長字串只在記憶體使用量和操作速度方面才象引用,但是如果你改變了字串的值,初始值將不受影響。相反,如果通過引用傳遞長字串,那麼串的初始值就可以改變。

Delphi 3增加了一種新的參數:out。out參數沒有初始值,只是用來返回一個值。out參數應只用於COM過程和函數,一般情況下最好使用更有效var參數。除了沒有初始值這一點之外,out參數與var參數相同。

常量參數

除了引用參數外,還有一種參數叫常量參數。由於不允許在常式中給常量參數賦新值,因此編譯器能最佳化常參的傳遞過程。編譯器會選用一種與引用參數相似的方法編譯常參(C++術語中的常量引用),但是從表面上看常參又與值參相似,因為常參初始值不受常式的影響。

事實上,如果編譯下面有點可笑的代碼,Delphi將出現錯誤:

function DoubleTheValue (const Value: Integer): Integer;begin  Value := Value * 2;      // compiler error  Result := Value;end;
開放數組參數

與C語言不同,Pascal 函數及過程的參數個數是預定的。如果參數個數預先沒有確定,則需要通過開放數組來實現參數傳遞。

一個開放數組參數就是一個固定類型開放數組的元素。 也就是說,參數類型已定義,但是數組中的元素個數是未知數。見下例:

function Sum (const A: array of Integer): Integer;var  I: Integer;begin  Result := 0;  for I := Low(A) to High(A) do    Result := Result + A[I];end;

上面通過High(A)擷取數組的大小,注意其中函數傳回值 Result的應用, Result用來儲存臨時值。你可通過一個整數運算式組成的數組來調用該函數:

X := Sum ([10, Y, 27*I]);

給定一個整型數組,數組大小任意,你可以直接把它傳遞給帶開放數組參數的常式,此外你也可以通過Slice 函數,只傳遞數組的一部分元素(傳遞元素個數由Slice 函數的第二個參數指定)。下面是傳遞整個數組參數的例子:

var  List: array [1..10] of Integer;  X, I: Integer;begin  // initialize the array  for I := Low (List) to High (List) do    List [I] := I * 2;  // call  X := Sum (List);

如果你只傳遞數組的一部分,可使用Slice 函數,如下:

X := Sum (Slice (List, 5));

在Delphi 4中,給定類型的開放數組與動態數組完全相容(動態數組將在第8章中介紹)。動態數組的文法與開放數組相同,區別在於你可以用諸如array of Integer指令定義變數,而不僅僅是傳遞參數。

類型變化的開放數組參數

除了類型固定的開放數組外,Delphi 還允許定義類型變化的甚至無類型的開放數組。這種特殊類型的數組元素可隨意變化,能很方便地用作傳遞參數。

技術上,array of const 類型的數組就能實現把不同類型、不同個數元素組成的數組一下子傳遞給常式。如下面Format 函數的定義(第七章中你將看到怎樣使用這個函數):

function Format (const Format: string;  const Args: array of const): string;

上面第二個參數是個開放數組,該數組元素可隨意變化。如你可以按以下方式調用這個函數:

N := 20;S := 'Total:';Label1.Caption := Format ('Total: %d', [N]);Label2.Caption := Format ('Int: %d, Float: %f', [N, 12.4]);Label3.Caption := Format ('%s %d', [S, N * 2]);

從上可見,傳遞的參數可以是常量值、變數值或一個運算式。聲明這類函數很簡單,但是怎樣編寫函數代碼呢?怎樣知道參數類型呢?對類型可變的開放數組,其數組元素與TVarRec 類型元素相容。

注意:不要把TVarRec 記錄類型和Variant 類型使用的TVarData 記錄類型相混淆。這兩種類型用途不同,而且互不相容。甚至可容納的資料類型也不同,因為TVarRec 支援Delphi 資料類型,而TVarData 支援OLE 資料類型。

TVarRec 記錄類型結構如下:

type  TVarRec = record    case Byte of      vtInteger:    (VInteger: Integer; VType: Byte);      vtBoolean:    (VBoolean: Boolean);      vtChar:       (VChar: Char);      vtExtended:   (VExtended: PExtended);      vtString:     (VString: PShortString);      vtPointer:    (VPointer: Pointer);      vtPChar:      (VPChar: PChar);      vtObject:     (VObject: TObject);      vtClass:      (VClass: TClass);      vtWideChar:   (VWideChar: WideChar);      vtPWideChar:  (VPWideChar: PWideChar);      vtAnsiString: (VAnsiString: Pointer);      vtCurrency:   (VCurrency: PCurrency);      vtVariant:    (VVariant: PVariant);      vtInterface:  (VInterface: Pointer);  end;

每種記錄都有一個VType 域,乍一看不容易發現,因為它與實際意義的整數型別資料(通常是一個引用或一個指標)放在一起,只被聲明了一次。

利用上面資訊我們就可以寫一個能操作不同類型資料的函數。下例的SumAll 函數,通過把字串轉成整數、字元轉成相應的序號、True布爾值加一,計算不同類型資料的和。這段代碼以一個case語句為基礎,雖然不得不經常通過指標取值,但相當簡單,:

function SumAll (const Args: array of const): Extended;var  I: Integer;begin  Result := 0;  for I := Low(Args) to High (Args) do    case Args [I].VType of      vtInteger: Result :=        Result + Args [I].VInteger;      vtBoolean:        if Args [I].VBoolean then          Result := Result + 1;      vtChar:        Result := Result + Ord (Args [I].VChar);      vtExtended:        Result := Result + Args [I].VExtended^;      vtString, vtAnsiString:        Result := Result + StrToIntDef ((Args [I].VString^), 0);      vtWideChar:        Result := Result + Ord (Args [I].VWideChar);      vtCurrency:        Result := Result + Args [I].VCurrency^;    end; // caseend;

我已在例OpenArr中加了這段代碼,該例在按下設定的按鈕後調用SumAll 函數。

procedure TForm1.Button4Click(Sender: TObject);var  X: Extended;  Y: Integer;begin  Y := 10;  X := SumAll ([Y * Y, 'k', True, 10.34, '99999']);  ShowMessage (Format (    'SumAll ([Y*Y, ''k'', True, 10.34, ''99999'']) => %n', [X]));end;

Delphi 調用協定

32位的Delphi 中增加了新的參數傳遞方法,稱為fastcall:只要有可能,傳遞到CPU寄存器的參數能多達三個,使函數叫用作業更快。這種快速調用協定(Delphi 3確省方式)可用register 關鍵字標示。

問題是這種快速調用協定與Windows不相容,Win32 API 函數必須聲明使用stdcall 調用協定。這種協定是Win16 API使用的原始Pascal 調用協定和C語言使用的cdecl 調用協定的混合體。

除非你要調用外部Windows函數或定義Windows 回呼函數,否則你沒有理由不用新增的快速調用協定。 在後面你會看到使用stdcall 協定的例子,在Delphi協助檔案的Calling conventions 主題下,你能找到有關Delphi調用協定的總結內容。

什麼是方法?

如果你使用過Delphi 或讀過Delphi 手冊,大概已經聽說過“方法”這個術語。方法是一種特殊的函數或過程,它與類這一資料類型相對應。在Delphi 中,每處理一個事件,都需要定義一個方法,該方法通常是個過程。不過一般“方法”是指與類相關的函數和過程。

你已經在本章和前幾章中看到了幾個方法。下面是Delphi 自動添加到表單原始碼中的一個空方法:

procedure TForm1.Button1Click(Sender: TObject);begin  {here goes your code}end;
Forward 聲明

當使用一個標識符(任何類型)時,編譯器必須已經知道該標識符指的是什麼。為此,你通常需要在常式使用之前提供一個完整的聲明。然而在某些情況下可能做不到這一點,例如過程A調用過程B,而過程B又調用過程A,那麼你寫過程代碼時,不得不調用編譯器尚未看到其聲明的常式。

欲聲明一個過程或函數,而且只給出它的名字和參數,不列出其實現代碼,需要在句尾加forward 關鍵字:

procedure Hello; forward;

在後面應該補上該過程的完整代碼,不過該過程代碼的位置不影響對它的調用。下面的例子沒什麼實際意義,看過後你會對上述概念有所認識:

procedure DoubleHello; forward;procedure Hello;begin  if MessageDlg ('Do you want a double message?',      mtConfirmation, [mbYes, mbNo], 0) = mrYes then    DoubleHello  else    ShowMessage ('Hello');end;procedure DoubleHello;begin  Hello;  Hello;end;

上述方法可用來寫遞迴調用:即DoubleHello 調用Hello,而Hello也可能調用DoubleHello。當然,必須設定條件終止這個遞迴,避免棧的溢出。上面的代碼可以在例DoubleH 中找到,只是稍有改動。

儘管 forward 過程聲明在Delphi中不常見,但是有一個類似的情況卻經常出現。當你在一個單元(關於單元的更多內容見下一章)的interface 部分聲明一個過程或一個函數時,它被認為是一個forward聲明,即使沒有forward關鍵字也一樣。實際上你不可能把整個常式的代碼放在interface 部分,不過你必須在同一單元中提供所聲明常式的實現。

類內部的方法聲明也同樣是forward聲明,當你給表單或其組件添加事件時, Delphi會自動產生相應的代碼。在TForm 類中聲明的事件是forward 聲明,事件代碼放在單元的實現部分。下面摘錄的原始碼中有一個Button1Click 方法聲明:

type  TForm1 = class(TForm)    ListBox1: TListBox;    Button1: TButton;    procedure Button1Click(Sender: TObject);  end;
過程類型

Object Pascal 的另一個獨特功能是可定義過程類型。過程類型屬於語言的進階功能,Delphi 程式員不會經常用到它。因為後面章節要討論相關的內容(尤其是“方法指標” Delphi用得特別多),這裡不妨先瞭解一下。如果你是初學者,可以先跳過這部分,當學到一定程度後再回過頭閱讀這部分。

Pascal 中的過程類型與C語言中的函數指標相似。過程類型的聲明只需要參數列表;如果是函數,再加個傳回值。例如聲明一個過程類型,該類型帶一個通過引用傳遞的整型參數:

type  IntProc = procedure (var Num: Integer);

這個過程類型與任何參數完全相同的常式相容(或用C語言行話來說,具有相同的函數特徵)。下面是一個相容常式:

procedure DoubleTheValue (var Value: Integer);begin  Value := Value * 2;end;

注意:在16位Delphi中,如果要將常式用作過程類型的實際值,必須用far指令聲明該常式。

過程類型能用於兩種不同的目的:聲明過程類型的變數;或者把過程類型(也就是函數指標)作為參數傳遞給另一常式。利用上面給定的類型和過程聲明,你可以寫出下面的代碼:

var  IP: IntProc;  X: Integer;begin  IP := DoubleTheValue;  X := 5;  IP (X);end;

這段代碼與下列代碼等效:

var  X: Integer;begin  X := 5;  DoubleTheValue (X);end;

上面第一段代碼明顯要複雜一些,那麼我們為什麼要用它呢?因為在某些情況下,調用什麼樣的函數需要在實際中決定,此時程式類型就很有用。這裡不可能建立一個複雜的例子來說明這個問題,不過可以探究一下簡單點的例子,該例名為ProcType。該例比前面所舉的例子都複雜,更接近實際應用。

6.3所示,建立一個工程,在上面放兩個radio按鈕和一個push按鈕。例中有兩個過程,一個過程使參數的值加倍,與前面的DoubleTheValue過程相似;另一個過程使參數的值變成三倍,因此命名為TripleTheValue

procedure TripleTheValue (var Value: Integer);begin  Value := Value * 3;  ShowMessage ('Value tripled: ' + IntToStr (Value));end;

兩個過程都有結果顯示,讓我們知道他們已被調用。這是一個簡單的程式調試技巧,你可以用它來檢測某一程式碼片段是否或何時被執行,而不用在代碼中加斷點。

當使用者按Apply 按鈕,程式會根據radio按鈕狀態選擇執行的過程。實際上,當表單中有兩個radio按鈕時,你只能選擇一個,因此你只需要在Apply 按鈕的OnClick 事件中添加代碼檢測radio按鈕的值,就能實現程式要求。不過為了示範過程類型的使用,我捨近求遠選擇了麻煩但有趣的方法:只要使用者選中其中一個radio按鈕,按鈕對應的過程就會存入過程變數:

procedure TForm1.DoubleRadioButtonClick(Sender: TObject);begin  IP := DoubleTheValue;end;

當使用者按Apply 按鈕,程式就執行過程變數儲存的過程:

procedure TForm1.ApplyButtonClick(Sender: TObject);begin  IP (X);end;

為了使三個不同的函數能訪問IP和 X變數,需要使變數在整個表單單元中可見,因此不能聲明為局部變數(在一個方法中聲明)。一個解決辦法是,把這些變數放在表單聲明中:

type  TForm1 = class(TForm)    ...  private    { Private declarations }    IP: IntProc;    X: Integer;  end;

學完下一章,你會更清楚地瞭解這段代碼的意思,目前只要能知道怎樣添加過程類型定義、怎樣修改相應的代碼就行了。為了用適當的值初始化上面代碼中的兩個變數,你可以調用表單的OnCreate 事件(啟用表單後,在Object Inspector中選擇這一事件,或者雙擊表單)。此外最好仔細看一看上例完整的原始碼。

在第九講的 Windows 回呼函數一節,你能看到使用過程類型的執行個體

函數重載

重載的思想很簡單:編譯器允許你用同一名字定義多個函數或過程,只要它們所帶的參數不同。實際上,編譯器是通過檢測參數來確定需要調用的常式。

下面是從VCL的數學單元(Math Unit)中摘錄的一系列函數:

function Min (A,B: Integer): Integer; overload;function Min (A,B: Int64): Int64; overload;function Min (A,B: Single): Single; overload;function Min (A,B: Double): Double; overload;function Min (A,B: Extended): Extended; overload;

當調用方式為Min (10, 20)時,編譯器很容易就能判定你調用的是上列第一個函數,因此傳回值也是個整數。

聲明重載函數有兩條原則:

  • 每個常式聲明後面必須添加overload 關鍵字。
  • 常式間的參數個數或(和)參數類型必須不同,傳回值不能用於區分各常式。

下面是ShowMsg 過程的三個重載過程。我已把它們添加到例OverDef 中(一個說明重載和確省參數的應用程式):

procedure ShowMsg (str: string); overload;begin  MessageDlg (str, mtInformation, [mbOK], 0);end;procedure ShowMsg (FormatStr: string;  Params: array of const); overload;begin  MessageDlg (Format (FormatStr, Params),    mtInformation, [mbOK], 0);end;procedure ShowMsg (I: Integer; Str: string); overload;begin  ShowMsg (IntToStr (I) + ' ' + Str);end;

三個過程分別用三種不同的方法格式化字串,然後在資訊框中顯示字串。下面是三個常式的調用:

ShowMsg ('Hello');ShowMsg ('Total = %d.', [100]);ShowMsg (10, 'MBytes');

令我驚喜的是Delphi的代碼參數技術與重載過程及函數結合得非常好。當你在常式名後面鍵入左圓括弧時,視窗中會顯示所有可用常式的參數列表,當你輸入參數時,Delphi會根據所輸入參數的類型過濾參數列表。從圖6.4你可看到,當開始輸入一個常量字串時,Delphi只顯示第一個參數為字串的兩個ShowMsg常式參數列表,濾掉了第一個參數為整數的常式。

重載常式必須用overload關鍵字明確標示,你不能在同一單元中重載沒有overload標示的常式,否則會出現錯誤資訊: "Previous declaration of '<name>' was not marked with the 'overload' directive."。不過你可以重載在其他單元中聲明的常式,這是為了與以前的Delphi版本相容,以前的Delphi版本允許不同的單元重用相同的常式名。無論如何,這是常式重載的特殊情況不是其特殊功能,而且不小心會出現問題。

例如在一個單元中添加以下代碼:

procedure MessageDlg (str: string); overload;begin  Dialogs.MessageDlg (str, mtInformation, [mbOK], 0);end;

這段代碼並沒有真正重載原始的MessageDlg 常式,實際上如果鍵入:

MessageDlg ('Hello');

你將得到一個有意思的錯誤訊息,告訴你缺少參數。調用本地常式而不是VCL的唯一途徑是明確標樣本程所在單元,這有悖於常式重載的思想:

OverDefF.MessageDlg ('Hello');

確省參數

Delphi 4 中添加了一個新功能,即允許你給函數的參數設定確省值,這樣調用函數時該參數可以加上,也可以省略。下例把應用程式全程對象的MessageBox 方法重新封裝了一下,用PChar 替代字串,並設定兩個確省值:

procedure MessBox (Msg: string;  Caption: string = 'Warning';  Flags: LongInt = mb_OK or mb_IconHand);begin  Application.MessageBox (PChar (Msg),    PChar (Caption), Flags);end;

使用這一定義,你就可以用下面任一種方式調用過程:

MessBox ('Something wrong here!');MessBox ('Something wrong here!', 'Attention');MessBox ('Hello', 'Message', mb_OK);

從圖6.5中可以看到,Delphi的 代碼參數提示條會用不同的風格顯示確省值參數,這樣你就很容易確定哪個參數是可以省略的。

注意一點,Delphi 不產生任何支援確省參數的特殊代碼,也不建立常式的多份拷貝,預設參數是由編譯器在編譯時間添加到調用常式的代碼中。

使用確省參數有一重要限定:你不能“跳過”參數,如省略第二個參數後,不能把第三個參數傳給函數:

MessBox ('Hello', mb_OK); // error  

確省參數使用主要規則:調用時你只能從最後一個參數開始進行省略,換句話說,如果你要省略一個參數,你必須省略它後面所有的參數。

確省參數的使用規則還包括:

  • 帶確省值的參數必須放在參數表的最後面。
  • 確省值必須是常量。顯然,這限制了確省參數的資料類型,例如動態數組和介面類型的確省參數值只能是 nil;至於記錄類型,則根本不能用作確省參數。
  • 確省參數必須通過值參或常參傳遞。引用參數 var不能有預設值。

如果同時使用確省參數和重載可能會出現問題,因為這兩種功能可能發生衝突。例如把以前ShowMsg 過程改成:

procedure ShowMsg (Str: string; I: Integer = 0); overload;begin  MessageDlg (Str + ': ' + IntToStr (I), mtInformation, [mbOK], 0);end;

編譯時間編譯器不會提出警告,因為這是合法的定義。

然而編譯調用語句:

ShowMsg ('Hello');

編譯器會顯示 Ambiguous overloaded call to 'ShowMsg'.( 不明確重載調用ShowMsg)。注意,這條錯誤資訊指向新定義的重載常式程式碼之前。實際上,用一個字串參數無法調用ShowMsg 過程,因為編譯器搞不清楚你是要調用只帶字串參數的ShowMsg 過程,還是帶字串及整型確省參數的過程。遇到這種問題時,編譯器不得不停下來,要求你明確自己的意圖。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.