3.2 过程和函数
3.2 过程和函数
过程和函数就是用来完成特定功能的一些代码组成的程序块。它就好像是公共汽车,我们乘坐公共汽车,可以到达目的地。如果坐一路公共汽车还不能到达终点,我们还可以在中途换乘,就相当于调用多个过程和函数来完成一项功能。
过程和函数的惟一区别就是:函数有返回值而过程没有。
在Delphi在线帮助的一些地方,常把函数和过程合称例程(routine);但是在IDE的很多地方又用过程(procedures)来合称它们。本书的某些地方使用过程来合称函数和过程。如果函数和过程隶属于类或者对象,那么就应该叫做方法。在本节里,我们用过程来通称过程和函数。
3.2.1 作用域
一个Unit(单元)中,声明于interface部分(即interface和implementation关键字之间的过程称为全局过程。另一个单元uses(引用)这个单元后,可以调用这些全局过程。
当然了,也只能在interface部分作过程的声明。一个过程在没有声明的情况下,可以在implementation部分直接实现,从而成为一个局部过程。局部过程只能在本单元调用,且调用位置必须在它的实现后面。
变量的作用域也是类似的,如果定义在interface部分,那么是全局的(典型的如:var Form1: TForm1),否则是局部的,只能在本单元使用。
我们看单元Unit1的全部代码如下:
unit Unit1;
interface
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls,
Forms, Dialogs, StdCtrls;
{位置A}
type
TForm1 = class(TForm)
Button1: TButton;
procedure Button1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;
{位置B}
{声明一个全局过程GlobalProc。一个全局过程可以在位置A、B、C三个地方声明,效果是一
样的}
procedure GlobalProc;
var
Form1: TForm1;
{位置C}
implementation
{$R *.dfm}
{实现一个局部过程LocalProc,其他单元引用Unit1后是看不到这个过程的}
procedure LocalProc;
begin
ShowMessage('LocalProc被调用');
end;
procedure GlobalProc;
begin
ShowMessage('调用LocalProc');
{必须在局部过程的实现位置以后调用。否则,由于局部过程没有声明而导致无法定位它。如
果你将GlobalProc的实现移到LocalProc的实现之前,则会产生编译错误
——"LocalProc未被定义"}
LocalProc;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
GlobalProc;
end;
end.
还可以在过程内部实现子过程,子过程的作用域就更小了,只能在父过程中被调用。如果一个过程有多个子过程,那么实现位置更靠后的子过程可以调用它前面的子过程。比如:
procedure TForm1.Button2Click(Sender: TObject);
var
S: String;
{实现Button2Click的子过程ShowInfo。ShowInfo只能在Button2Click中被调用}
procedure ShowInfo(Info: String);
begin
ShowMessage(Info);
end;
begin
S := 'lxpbuaa';
ShowInfo(S);
end;
3.2.2 参数传递
声明/实现一个过程使用的参数称为形式参数(简称形参),调用过程时传入的参数称为实际参数(简称实参)。
{ Info是形参}
procedure ShowInfo(Info: String);
begin
ShowMessage(Info);
end;
var
S: String;
begin
S := 'lxpbuaa';
{S是实参}
ShowInfo(S);
end;
参数传递分两种:按值(by val)和引用(by ref)。这两种方式的本质区别是:
按值传递时,形参和实参是两个变量,它们开始时的值是相同的,即实参的数据被拷贝一份传递给了形参。所以此时,形参的改变不会影响到实参。
引用传递时,形参和实参是同一个变量,可以将它们之一看做是另一个的别名。所以此时,形参改变时,实参跟着改变。
默认情况下,参数是按值传递的,传递的是数据拷贝;如果加了var前缀,则成了引用传递。
我们看如下例子:
procedure TForm1.ByVal(I: Integer); {按值传递I}
begin
ShowMessage(IntToStr(Integer(@I)));
{取得形参所在地址。你会发现它和实参地址是不同的,因为此时实参和形参是不同的两个变量}
I := I + 1;
end;
procedure TForm1.ByRef(var I: Integer); {引用传递I}
begin
ShowMessage(IntToStr(Integer(@I)));
{取得形参所在地址。你会发现它和实参地址是相同的,因为此时实参和形参是同一个变量}
I := I + 1;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
I: Integer;
begin
I := 1;
ShowMessage(IntToStr(Integer(@I))); {取得实参所在地址}
ByVal(I); { I =1}
ByRef(I); { I =2}
end;
按值传递的参数可以指定默认值,比如上面的ByVal可以是这样:
procedure ByVal(I: Integer = 0);
调用它时可以省掉有默认值的参数:ByVal。
带默认值的参数必须位于参数列表的最后,如:
procedure ByVal(I: Integer = 0; B: Boolean);
是不行的,应该改为:
procedure ByVal(B: Boolean; I: Integer = 0);
因为默认值必须是一个常数表达式,所以dynamic-array、procedural、class、class-reference和interface等参数只能指定nil默认值;而record、variant、file和static-array等类型的参数则根本不能指定默认值。
如果按值传递一个指针类型的参数,情况会变得复杂而又很有意思。此时,实际传递的是什么呢?是实际数据的拷贝吗?不,是指针的拷贝,也就是说形参和实参是两个指针,不过这两个指针指向了相同地址。所以这时候,形参和实参可以共享它们指向地址中的数据,但如果改变了形参的指针指向,实参的指针指向不能跟着改变。那么总结一下,就是:
按值传递指针参数时,实参和形参可以共享指针指向地址中的数据,但是不能共享指针本身的指向。而引用传递时,因为实参和形参是同一个变量,因此实现完全共享。看下面的例子:
procedure TForm1.ByVal(Obj: TObject);
begin
Obj := Button1;
{改变形参指针指向,实参的指针指向不会跟着改变,因为它们是两个变量。如果仅仅是改变
Obj的属性而不改变指向,则实参的属性会跟着改变}
end;
procedure TForm1.ByRef(var Obj: TObject);
begin
Obj := Button1;
{改变形参指针指向,实参的指针指向跟着改变,因为它们是同一个变量}
end;
procedure TForm1.Button1Click(Sender: TObject);
var
Obj: TObject;
begin
Obj := Self;
{Self即Form1,所以此时实参Obj的类名(ClassName)是"TForm1"}
ByVal(Obj); {按值传递指针变量Obj}
ShowMessage(Obj.ClassName); {显示类名"TForm1"}
ByRef(Obj); {引用传递指针变量Obj}
ShowMessage(Obj.ClassName); {显示类名"TButton1"}
end;
上面讲了这么多,最根本的还是一句话:按值传递时,形参和实参是两个变量;引用传递时,形参和实参是同一个变量。抓住这句话,就等于抓住了一切。
相信你还看到过如下格式的参数声明:
function CompareStr(const S1, S2: string): Integer;
function TryStrToInt(const S: string; out Value: Integer): Boolean;
其中使用了const和out关键字。如果你没有看到过这样的声明,也不要紧,它们是真实存在的。
const声明的参数是按值传递的,而且形参不能被改变。
out声明的参数是引用传递的,主要用于定义输出参数,也就是说不需要输入值(即实参不需要初始化),实参传递给形参的值被忽略。
如果用const修饰指针参数,那么只能通过形参修改指针地址里的数据而不能修改指针本身的指向。例如对于一个const对象参数,可以修改其属性,但是不能将它指向其他对象。例如:
procedure ShowInfo(const Form: TForm);
begin
{以下一句不能通过,编译器提示:[Error] Unit1.pas(28): Left side cannot be
assigned to}
{Form := Form1;}
{但是通过其属性或者方法修改隶属于Form的数据}
Form.Caption := 'lxpbuaa';
ShowMessage(Form.Caption);
end;
在本小节的最后,还不得不提及一种很特殊的参数类型:无类型参数(Untyped parameters)。
声明时没有指定数据类型的参数称为无类型参数。因此,从语法上讲,无类型参数可以接收任何类型的数据。
无类型参数必须加const、out或var前缀;无类型参数不能指定默认值。
如以下一些Delphi定义的过程都使用了无类型参数:
procedure SetLength(var S; NewLength: Integer); {参数S}
procedure Move(const Source;var Dest;Count:Integer); {参数Source、Dest}
procedure TStream.WriteBuffer(const Buffer; Count: Longint);{参数Buffer}
所谓无类型参数可以接收任何类型的值,只是从语法角度而言的。或者说,理论上我们可以实现一个可以使用任何类型变量作为参数的过程,但是实际上没有必要,也不可能做到。
打个比方说,我们想造一辆可以装载任何物体的汽车。因为是“任何物体”,所以物体可能是任何形状,于是这辆车必须没有车篷,除了在几个车轮上铺一个足够大(足够大就已经是个大问题了)的平板外,不能再有任何东西。这时候,这个平板就可以看做是无类型的,因为它上面可以坐人、摆一张桌子,也可以赶一些动物上去站着或者躺着。尽管它可以承载很多种类的东西,但是也是有限制的,比如不能放一座山、也无法容纳1万头猪。
所以无类型参数的类型往往是有一定限制的。比如SetLength的参数S只能是字符串、动态数组等。
这种限制一般是在过程的实现中完成的,在运行时检查参数值的实际类型。对于与开发环境关系紧密的参数,限制也可以构筑在编译器里。
使用无类型参数的原因是无法在声明时使用一个统一的类型来描述运行时可能的类型,如SetLength的参数S可以是字符串和动态数组,而并没有一个统一的类型来代表字符串和动态数组类型,所以干脆声明为无类型。而将类型限制放到别的地方实现(如编译器)。例如SetLength的限制规则是写在编译器中的,它只能作用于长字符串或者动态数组。你企图完成下面的功能时:
var
I: Integer;
begin
SetLength(I, 10);
end;
编译器编译时将给出错误信息:[Error] Unit1.pas(35): Incompatible types。导致编译中断。
小结
本小节的内容比较重要,重点是理解参数按值传递和引用传递的本质:按值传递时,形参和实参是两个变量;引用传