引用参数
Pascal 例程的传递参数可以是值参也可以是引用参数。值参传递是缺省的参数传递方式:即将值参的拷贝压入栈中,例程使用、操纵的是栈中的拷贝值,不是原始值。
当通过引用传递参数时,没有按正常方式把参数值的拷贝压栈(避免拷贝值压栈一般能加快程序执行速度),而是直接引用参数原始值,例程中的代码也同样访问原始值,这样就能在过程或函数中改变参数的值。引用参数用关键字var 标示。
下面是利用引用传递参数的例子,引用参数用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));
图 6.1: 单击 Partial Slice 按钮显示的结果
在Delphi 4中,给定类型的开放数组与动态数组完全兼容。动态数组的语法与开放数组相同,区别在于你可以用诸如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;
Delphi 调用协定
32位的Delphi 中增加了新的参数传递方法,称为fastcall:只要有可能,传递到CPU寄存器的参数能多达三个,使函数调用操作更快。这种快速调用协定(Delphi 3确省方式)可用register 关键字标示。
问题是这种快速调用协定与Windows不兼容,Win32 API 函数必须声明使用stdcall 调用协定。这种协定是Win16 API使用的原始Pascal 调用协定和C语言使用的cdecl 调用协定的混合体。
除非你要调用外部Windows函数或定义Windows 回调函数,否则你没有理由不用新增的快速调用协定。
什么是方法?
如果你使用过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。当然,必须设置条件终止这个递归,避免栈的溢出。
尽管 forward 过程声明在Delphi中不常见,但是有一个类似的情况却经常出现。当你在一个单元(关于单元的更多内容见下一章)的interface 部分声明一个过程或一个函数时,它被认为是一个forward声明,即使没有forward关键字也一样。实际上你不可能把整个例程的代码放在interface 部分,不过你必须在同一单元中提供所声明例程的实现。
类内部的方法声明也同样是forward声明,当你给窗体或其组件添加事件时, Delphi会自动产生相应的代码。在TForm 类中声明的事件是forward 声明,事件代码放在单元的实现部分。下面摘录的源代码中有一个Button1Click 方法声明:
type
TForm1 = class(TForm)
ListBox1: TListBox;
Button1: TButton;
procedure Button1Click(Sender: TObject);
end;