Delphi 之 第六課 過程與函式
這講是核心重點*****
什麼是過程?什麼又是函式?過程和函式在delphi中無處不再。過程簡單的理解就是單擊一個按鈕這就是一個過程,函式和過程不一樣的地方就是函式能把這個過程的結果返回給我們,過程的關鍵字用procedure ,函式的關鍵字Function
下面就具體講解過程與函式的定義
過程與函式
Pascal中的例程有兩種形式:過程和函式。理論上說,過程是你要求計算機執行的操作,函式是能返回值的計算。兩者突出的不同點在於:函式能返回計算結果,即有一個返回值,而過程沒有。兩種型別的例程都可以帶多個給定型別的引數。
不過實際上函式和過程差別不大,因為你可以呼叫函式完成一系列操作,跳過其返回值(用可選的出錯程式碼或類似的東西代替返回值);也可以通過過程的引數傳遞計算結果(這種引數稱為引用,下一部分會講到)。
下例定義了一個過程、兩個函式,兩個函式的語法略有不同,結果是完全相同的。
procedure Hello;
begin
ShowMessage ('Hello world!');
end;
function Double (Value: Integer) : Integer;
begin
Double := Value * 2;
end;
// or, as an alternative
function 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; // case
end;
我已在例OpenArr中加了這段程式碼,該例在按下設定的按鈕後呼叫SumAll 函式。
procedure TForm1.Button6Click(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;
To:chris2019(牛虻---最後的匈奴)
---------------------
---------------------
TForm1=Class(TForm);
//這句話指是TForm1是從TForm繼乘過來的一個子類
Button1=TButton
//這種用法好象沒有
TButton1=Class(TButton)
//這句話也是指TButton1是從TButton中繼乘過來的一個子類
var
Form1:TForm1;
//這是指定義了一個TForm1類的物件變數。
學完下一章,你會更清楚地瞭解這段程式碼的意思,目前只要能知道怎樣新增過程型別定義、怎樣修改相應的程式碼就行了。為了用適當的值初始化上面程式碼中的兩個變數,你可以呼叫窗體的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 過程,還是帶字串及整型確省引數的過程。遇到這種問題時,編譯器不得不停下來,要求你明確自己的意圖。
結束語
過程和函式是程式設計的一大關鍵,Delphi 中的方法就是與類及物件關聯的過程和函式。
下面幾章將從字串開始詳細講解Pascal的一些程式設計元素。