(delphi11最新学习资料) Object Pascal 学习笔记---第4章第4节(函数的高级特性)

4.4 函数的高级特性

​ 到目前为止,我已经介绍了与函数相关的核心功能,但还有一些高级功能值得探索。不过,如果你确实是软件开发方面的新手,你可能会想暂时跳过本章的其余部分,转到下一章。

4.4.1 Object Pascal 的调用约定
每当你的代码需要调用函数时,双方需要就参数从调用者传递给被调用者的实际方式达成一致,这就是所谓的调用约定。一般来说,函数调用是通过堆栈内存区域传递参数(并期望返回值)。不过,参数和返回值在堆栈中的顺序会因编程语言和平台的不同而改变,大多数编程语言都能使用多种不同的调用约定。

很久以前,32 位版本的 Delphi 引入了一种新的参数传递方法,即 “fastcall”: 只要有可能,最多可以在 CPU 寄存器中传递三个参数,从而使函数调用速度大大提高。Object Pascal 默认使用这种快速调用约定,但也可以通过使用register关键字来请求。

Fastcall 是默认的调用约定,使用这个调用约定的函数与外部库不兼容,如 Win32 中的 Windows API 函数。Win32 API 的函数必须使用 stdcall(标准调用)调用约定来声明,它是 Win16 API 的原始 pascal 调用约定和 C 语言的 cdecl 调用约定的混合体。Object Pascal 支持所有这些调用约定,但除非需要调用不同语言编写的库,如系统库,否则很少会使用与默认约定不同的调用约定。

需要摒弃默认快速调用约定的一个典型情况是需要调用平台的本地 API,根据操作系统的不同,需要使用不同的调用约定。即使是 Win64 也使用与 Win32 不同的模式,因此 Object Pascal 支持许多不同的选项,这里不值得详述。移动操作系统倾向于公开类,而不是本地函数,但即使在这些情况下,也必须考虑尊重特定调用约定的问题。

4.4.2 过程类型

​ Object Pascal 的另一个特点是存在过程类型。这实际上是一个高级的语言主题,只有少数程序员会使用。然而,由于我们将在后面的章节中将讨论相关主题(具体来说是方法指针,这是环境用于定义事件处理程序以及匿名方法的一种技术),在这里简要介绍一下是值得的。

​ 在 Object Pascal 中(但不在更传统的 Pascal 语言中),存在过程类型的概念(与 C 语言的函数指针概念类似——这是 C# 和 Java 等语言已经放弃的功能,因为它与全局函数和指针相关)。过程类型的声明指出参数列表,在函数的情况下还包括返回类型。例如,您可以使用以下代码声明一个新的过程类型,其中包含一个按引用传递的 Integer 参数:

type
  TIntProc = procedure(var Num: Integer);

​ 这个过程类型与具有完全相同参数的任何例程兼容(或者使用 C 的术语来说,具有相同的函数签名)。以下是一个兼容例程的示例:

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

​ 过程类型可以用于两种不同的目的:您可以声明过程类型的变量,或将过程类型(即函数指针)作为参数传递给另一个例程。鉴于前述类型和过程声明,您可以编写以下代码:

var
  IP: TIntProc;
  X: Integer;
begin
  IP := DoubleIt;
  X := 5;
  IP(X);
end;

这段代码与以下较短版本具有相同的效果:

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

​ 第一个版本显然更复杂,那么我们为什么要使用它,什么时候使用它呢?在某些情况下,能够延后决定实际调用哪个函数的能力会非常强大。我们可以建立一个复杂的示例来展示这种方法。不过,我更愿意让大家探索一个相当简单的示例,名为 ProcType。

​ 这个示例基于两个过程。一个过程用于将参数值加倍,就像我已经展示过的那样。第二个过程用于将参数值加三倍,因此被命名为 TripleIt:

procedure TripleIt(var Value: Integer);
begin
  Value := Value * 3;
end;

​ 我们不直接调用这些函数,而是将其中一个或另一个保存在程序类型变量中。当用户选择复选框时,该变量就会被修改,而当用户点击按钮时,当前过程就会以这种通用方式被调用。程序使用了两个初始化的全局变量(要调用的过程和当前值),因此这些值会随着时间的推移而保留。这是完整的代码,除去上面已经显示的实际过程的定义:

var
  IntProc: TIntProc = DoubleIt;
  Value: Integer = 1;

procedure TForm1.CheckBox1Change(Sender: TObject);
begin
  if CheckBox1.IsChecked then
    IntProc := TripleIt
  else
    IntProc := DoubleIt;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  IntProc(Value);
  Show(Value.ToString);
end;

​ 当用户更改复选框状态时,随后的所有按钮点击都将调用活动函数。因此,如果您按两次按钮,更改选择,然后再按两次按钮,您将先将当前值加倍两次,然后将其加倍两次,生成以下输出:

2 
4 
12 
36

​ 使用过程类型的另一个实际示例是当您需要将函数传递给像 Windows 这样的操作系统时(通常称为“回调函数”)。正如本节开头提到的,除了过程类型,Object Pascal 开发人员还使用方法指针(在第 10 章中介绍)和匿名方法(在第 15 章中介绍)。

注解:在面向对象的机制中,获得后期绑定的函数调用(即运行时可以改变的函数调用)的最常见的方法是使用虚方法。虚方法在 Object Pascal 中非常常见,而过程类型却很少使用。然而,技术基础在某种程度上是相似的。虚函数和多态性将在第 8 章中讨论。

4.4.3 外部函数声明

​ 外部声明是系统编程的另一个重要元素。外部声明最初用于将代码链接到用汇编语言编写的外部函数,在 Windows 编程中,外部声明已成为调用 DLL(动态链接库)函数的常用方法。外部函数声明意味着可以调用编译器或链接器无法完全使用的函数,但需要加载外部动态链接库并调用其中的一个函数。

注解:每当在您的 Object Pascal 代码中调用某个平台的 API 时,您失去了在任何其他平台上重新编译应用程序的能力,除非调用被平台特定的 $IFDEF 编译指令所包围。

​ 这就是您可以从 Delphi 应用程序中调用 Windows API 函数的方式。如果打开 Winapi.Windows 单元,您将找到许多函数声明和定义,如下所示:

// 前置声明
function GetUserName(lpBuffer: LPWSTR;
  var nSize: DWORD): BOOL; stdcall;
// 外部声明(而不是实际代码)
function GetUserName; external advapi32
  name 'GetUserNameW';

​ 由于 Windows 单元和许多其他系统单元中已经列出了这些声明,因此您很少需要编写类似刚才说明的声明。需要编写外部声明代码的唯一原因是调用自定义 DLL 中的函数,或调用平台 API 中未翻译的 Windows 函数。

​ 此声明意味着函数 GetUserName 的代码将以 GetUserNameW 的名称存储在 advapi32 动态库中(advapi32 是与 DLL 全名 "advapi32.dll "相关联的常量),因为此 API 函数既有 ASCII 版本,也有 WideString 版本。在外部声明中,我们可以指定我们的函数引用一个 DLL 函数,而该 DLL 函数最初的名称是不同的。

DLL 函数的延迟加载

​ 在 Windows 操作系统中,有两种方法可以调用 Windows SDK(或任何其他 DLL)的 API 函数:一种是让应用程序加载器解决所有外部函数的引用问题,另一种是编写特定代码来查找函数并在函数可用时执行它。

​ 前一种代码更容易编写(正如我们在上一节中所看到的):因为你所需要的只是外部函数声明。但是,如果你想调用的函数库或哪怕只有一个函数不可用,你的程序将无法在不提供该函数的操作系统版本上启动。

​ 动态加载允许更大的灵活性,但需要手动加载库,使用 GetProcAddress API 查找要调用的函数,并在将指针转换为适当类型后调用该函数。这种代码相当繁琐,而且容易出错。

​ 因此,Object Pascal 编译器和链接器专门支持 Windows 操作系统中的一项功能,而且一些 C++ 编译器已经使用了这项功能,即在调用函数之前延迟加载函数。这种声明的目的不是为了避免 DLL 的隐式加载(无论如何都要加载),而是为了允许在 DLL 中延迟绑定特定函数。

​ 基本上,你编写代码的方式与 DLL 函数的经典执行方式非常相似,但函数地址是在首次调用函数时解析的,而不是在加载时。这意味着,如果函数不可用,就会出现运行时异常,即 EExternalException。不过,一般情况下,您可以验证操作系统的当前版本或您要调用的特定库的版本,并提前决定是否要进行调用。

注解:如果你想要一个比异常更具体、更容易在全局级别上处理的方式,你可以挂钩延迟加载调用的错误机制,正如 Allen Bauer 在他的博客文章中解释的那样:https://blog.therealoracleatdelphi.com/2009/08/exceptional-procrastination_29.html

​ 从 Object Pascal 语言的角度来看,唯一的区别在于外部函数的声明,而不是编写:

function MessageBox;
  external user32 name 'MessageBoxW';

​ 现在可以编写(同样来自 Windows 单元中的实际示例):

function GetSystemMetricsForDpi(nIndex: Integer; dpi: UINT): Integer;
	stdcall; external user32 name 'GetSystemMetricsForDpi' delayed;

​ 在运行时,考虑到该 API 是首次添加到 Windows 10 1607 版本中,您可能希望编写如下代码:

if (TOSVersion.Major >= 10) and (TOSVersion.Build >= 14393) then
  NMetric := GetSystemMetricsForDpi(SM_CXBORDER, 96);

​ 这比在旧版本 Windows 上,没有延迟加载的情况下运行相同程序所需的代码要少得多。

​ 另一个相关的观察是,在构建自己的 DLL 并在Object Pascal 中调用它们时,只要对新函数使用延迟加载,就可以使用相同的机制,提供一个可以绑定到同一 DLL 的多个版本的单一可执行文件。

  • 52
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值