Delphi 自第 7 版以来已经有了长足的进步,目前已经到了最新版本的Delphi 12.2。随着现代编程技术的发展,Delphi 也引入了很多新特性,最常用的包括:
- 接口 Interfaces(Delphi 3 中引入)
- 匿名方法 Anonymous methods(Delphi 2009)
- 泛型 Generics(也是 Delphi 2009)。
当然也包括其他语言特性,例如多态性。
一、接口 Interface
Delphi 在很早支持接口,但很少有开发人员能很好利用它们。 接口使用依赖注入模式和依赖反转原则。 这两种技术都是非常强大的技术,可以帮助代码的可维护性。 要利用好它们,首先需要了解接口的工作原理。
首先,像这样声明一个接口:
Delphi
type
IAnimal = interface
['{C7982869-9293-41C2-8294-4DCE28623435}']
procedure Speak;
procedure MoveSlow;
procedure MoveFast;
end
需要使用 GUID(全球唯一标识符,快捷键:Ctrl+Shift+G)来唯一标识一个接口。每个接口都需要自己的 GUID(否则就只是一个 ID)。在类中实现接口的过程如下:
Delphi
type
01001000
TAnimal = class(TInterfacedObject, IAnimal)
protected
procedure Speak; virtual;
procedure MoveSlow; virtual;
procedure MoveFast; virtual;
end;
TDog = class(TAnimal)
protected
procedure Speak; override;
procedure MoveSlow; override;
procedure MoveFast; override;
end;
我继承自 TInterfacedObject,而不是 TObject,这样做是为了实现接口的最低要求,即三个特殊的方法(_AddRef、_Release 和 QueryInterface)。 TInterfacedObject为你实现了这些方法,看起来类似于下面这样:
Delphi
TInterfacedObject = class(TObject, IInterface)
Protected
FRefCount: Integer;
function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
function _AddRef: Integer; stdcall;
function _Release: Integer; stdcall;
end;
具体实现取决于您使用的 Delphi 版本。_AddRef 会增加引用计数 (FRefCount),_Release 会减少引用计数、如果达到零,_Release 还会销毁接口的内容。 TDog 继承自 TAnimal,因此也实现了 IAnimal。 所有的多态性的所有规则都以自然的方式适用于接口,而且接口之间也可以相互继承。 一个给定的类可以实现一系列的接口。 这就提供了一种多重继承,而不会给语言带来许多令人困惑的的麻烦。
接口提供了自动引用计数 (ARC),这是一种自动内存管理。自动内存管理,每次分配接口时,都会调用 _AddRef 方法都会被调用,而每次接口退出作用域或被分配都会调用 _Release 方法。 这意味着以下代码不会泄漏内存。
Delphi
var
Dog : IAnimal;
begin
Dog := TDog.Create;
Dog.Speak;
end;
当 Dog 变量被分配一个新的 TDog 时,该对象的引用计数为 1。对象的引用计数为 1,当存储过程结束时,引用计数会降为 0。这将导致 TDog 对象被销毁。 别担心,在这个示例中并没有真正销毁TDog。 在使用接口时,不使用 try...finally 子句和手动销毁可以大大减少需要编写的代码量,从而使代码更易于维护,编写速度也更快。 不过,这往往会给调试带来困难。
因为接口已经存在了很长时间,如果你在 Google 上搜索,会有很多教程和参考资料。
的教程和参考资料。 Coding in Delphi》(作者 Nick Hodges,2014 年)中也有一章是关于接口的,建议去看看。 就会很清楚接口原理了。
二、泛型 Generics
大多数开发人员不需要直接编写使用泛型的代码。相反,会利用使用泛型的库。 特别是
System.Generics.Collections 单元有几个预构建的容器,它们可以使你的代码更加类型安全,并减少你需要编写的代码量。 那么什么是泛型呢? 粗略地说,"这是一种编程风格,可以让你
编写算法,以便稍后指定类型,同时仍确保类型安全"。 另一种说法是 "参数化类型",就像一个存储过程可以有一个参数,这个参数有一个你传入的值,你也可以在其他地方指定该参数的类型。
举个例子。 假设有一个存储过程要检查数组中是否存在一个特定的整数值是否存在
Delphi
function IsPresentInArray(Value : integer; AnArray : array of
integer) : boolean;
var
I : integer;
begin
for I := low(AnArray) to High(AnArray) do
if Value = AnArray[i] then
Exit(True);
Result := False;
end;
And we can use this function like this:
procedure TForm7.Button1Click(Sender: TObject);
begin
if IsPresentInArray(3, [1,2,3,4,5]) then
ShowMessage('yes');
end
如果我们点击 按钮 1 ,它确实会显示 "是"。
这是一个简单明了的函数,您的库中也有类似的功能。 但是,如果您现在想在一个数组中查找一个字符串,或在一个floating-point 数字或 TBitmap 呢? 您需要为每种情况编写许多重载版本--这是大量重复性的工作。相反,我们可以将类型参数化,这就是泛型允许我们做的事情。
代码如下:
Delphi
function IsPresentInArray<T>(Value : T; AnArray : array of T) :
boolean;
var
I : integer;
begin
for I := low(AnArray) to High(AnArray) do
if Value = AnArray[i] then
Exit(True);
Result := False;
end
如果这段代码真的能工作(剧透--它不能工作--见下文),您可以按以下方式使用它:
Delphi
if IsPresentInArray<integer>(3, [1,2,3,4,5]) then
ShowMessage('yes');
Or
if IsPresentInArray<string>('yellow', ['red', 'blue', 'green',
'yellow']) then
ShowMessage('yes')
这里有两个问题。 首先,不可能将全局函数或过程过程变成通用的。 其次,我们不能简单地使用"="来表示相等。 为了解决第一个问题,我们可以使用类(或记录)。 第二个问题要求我们
使用 System.Generics.Defaults 单元中的 IComparer 接口。
Delphi
type
ArrayUtils = class
class function IsPresentInArray<T>(Value : T; AnArray : array
of T) : boolean;
end;
class function ArrayUtils.IsPresentInArray<T>(Value : T; AnArray :
array of T) : boolean;
var
I : integer;
lComparer: IEqualityComparer<T>;
begin
lComparer := TEqualityComparer<T>.Default;
for I := low(AnArray) to High(AnArray) do
if lComparer.Equals(Value, AnArray[i]) then
Exit(True);
Result := False;
end;
现在,我们可以随心所欲地使用我们的函数。 例如:
Delphi
if ArrayUtils.IsPresentInArray<TButton>(Button1, [Button1, Button2,
Button3, Button4]) then
ShowMessage('yes')
而且(假设所有按钮都已放到表单上),这很高兴地告诉我们 Button1 是数组的一部分。但是
Delphi
if ArrayUtils.IsPresentInArray<TButton>(Label1, [Button1, Button2,
Button3, Button4]) then
ShowMessage('yes')
甚至无法编译,我们得到编译器错误信息 "E2010不兼容类型:'TButton' 和 'TLabel'"。 因此,我们现在有了编译时类型安全。
在 Delphi 10.3 以后的版本中,在某些情况下可以自动确定类型(类型推断)。
Delphi
ArrayUtils.IsPresentInArray('yellow', ['red', 'blue', 'green',
'yellow'])
以上也是合法的。
三、匿名方法 Anonymous Methods
匿名方法大多是 "语法糖",你可以通过其他方法(函数指针,甚至是在程序上的过程)实现它们的大部分优点(函数指针,甚至只是类上的过程)。但它确实为你提供了额外的表达能力,我们不应该忽视它。匿名方法,顾名思义、没有名字的方法。 你可以将它们赋值给变量,也可以将它们作为参数传递。 让我们分析一个简单的例子:
Delphi
var
Greet : TProc<string>;
begin
Greet := procedure (value : string)
begin
ShowMessage('Hello ' + value);
end;
Greet('Alister');
end;
首先,让我们检查一下 Greet 变量;它的 TProc 类型是一个有用的速记符号,在 System.SysUtils 单元中定义了多个 TProc、TFunc 和一个TPredicate 类型。
Delphi
type
TProc<T> = reference to procedure (Arg1: T);
或者在我们的例子中,使用字符串,Greet 变量可以定义为:
Delphi
type
TGreet = reference to procedure(value : string);
var
Greet : TGreet
Greet 是一个变量,它是对TGreet过程的引用,过程接受一个字符串参数。 可以看出,TProc 的简写比它的完整定义更简洁。 回到最初的示例,我们为 Greet 赋值一个过程,然后调用该存储过程并传递一个参数。
让我们看看另一个更有用的例子:
使用匿名方法时的一个典型模式是将一个方法注入到另一个方法中。
Delphi
function ForEveryRecord(DataSet : TDataSet; Func :
TPredicate<TDataSet>) : boolean;
var
bm : TBookmark;
begin
result := True;
bm := DataSet.GetBookmark;
try
DataSet.First;
while not DataSet.EOF do
begin
if not Func(DataSet) then
Exit(False);
DataSet.Next;
end;
finally
DataSet.GotoBookmark(bm);
end;
end
该过程遍历数据集。 你可能会发现自己经常做这样的事情,并且一遍又一遍地写着同样的 while 循环。 假设 我们要对数据集(本例中为 cdsMonthlyEarnings)中名为 cost 的字段求和
Delphi
function TMonthEndReport.GetTotalEarnings: Currency;
var
TotalCost : Currency;
begin
TotalCost := 0;
ForEveryRecord(cdsMonthlyEarnings,
function(DataSet : TDataSet) : boolean
begin
result := True;
TotalCost := TotalCost +
DataSet.FieldByName('Cost').AsCurrency;
end
);
result := TotalCost;
end;
如果问候语的例子看起来很陌生,那么这个例子看起来就会非常奇怪。 我们将一个函数内联到过程中,然后对每条记录进行遍历,并对成本字段求和。
为什么要在匿名方法中使用布尔结果? 如果出现以下情况,这将非常方便。例如,我们正在搜索具有特定特征的记录。 如果找到了,我们可能想做一些处理,但不会遍历剩余的其余记录(这样会返回 false)。 例如:
Delphi
function TMonthEndReport.RecordIsValid(ds: TDataSet): boolean;
begin
//some processing that returns true for valid, and false
otherwise
result := ds.FieldByName('IsValid').AsBoolean;
end;
function TMonthEndReport.IsEveryRecordValid: boolean;
begin
result := ForEveryRecord(cdsMonthlyEarnings, RecordIsValid);
end;
在这里,我们将一个类的常规函数作为匿名方法传入。 如果函数对某条记录返回 false,ForEveryRecord 将停止对该记录的处理,并返回 false,从而使 IsEveryRecordValid 返回 false。
处理并返回 false。
变量捕捉
匿名方法的独特之处在于变量捕获。 这一点还没有使用过,但如果你不知道它是如何工作的,它
可能会让你大吃一惊。 为了解释它,先举一个例子:
Delphi
function VariableCapture : TProc;
var
x : integer;
begin
x := 5;
result := procedure
begin
x := x * 5;
ShowMessage(x.ToString);
end;
end;
该函数返回一个使用局部变量的匿名方法、变量 x 应在变量捕获函数终止时消失。
因此,您可能会认为如果匿名存储过程执行了,x 变量就会指向堆栈中的某个无效对象。
堆栈上的无效内容。 其实不然:
Delphi
var
p1, p2 : TProc;
begin
p1 := VariableCapture();
p2 := VariableCapture();
p1;
p1;
p2;
end;
在上述代码中,我们从变量捕获函数中获取了两个对匿名方法的引用。我们先调用 p1 两次,然后再调用 p2 一次,结果得到以下信息以下信息 25、125 和 25。 这是因为匿名方法会 "捕获 "本地变量,并在两次调用之间 "记住 "它,因此每次调用存储过程 p1,所以每次调用 p1 存储过程时,都会使用 x 的同一个实例、匿名方法是通过接口实现的,因此在上面的例子中,当匿名方法离开作用域时,它们就会被自动释放--连同任何捕获的变量。
我们对匿名方法的了解还只是皮毛,但它们非常有用。所以请积极学习更多。
匿名线程
匿名方法的一个常见用途是在后台线程中执行某些操作。这样就可以轻松执行任务,而不必担心创建 TThread 的后代,这样可以节省大量代码。
在下面的示例中,我们在一个匿名线程中压缩一些文件,当它在后台执行时,GUI 就会在该线程中运行。在后台执行时,图形用户界面不会被阻塞。 完成后,它会记录到备忘录中,表明它已经完成。 这可以作为循环的一部分运行,同时压缩将多个目录的内容压缩到不同的 zip 文件中。
在包含 TZipFile 的使用部分中加入 System.Zip 单元。
Delphi
var
ZipFileName : string;
DirectoryToZip : string;
begin
{... setup ZipFileName and DirectoryToZip ...}
TThread.CreateAnonymousThread( procedure
begin
TZipFile.ZipDirectoryContents(ZipFileName, DirectoryToZip);
TThread.Synchronize(nil, procedure
begin
memo1.Lines.Add('Zip Complete:' + ZipFileName);
end
);
end
).Start;
end;
内联变量和类型推断
这些功能是在 Delphi 10.3 Rio 中引入的,但当时 Error Insight 不支持内联变量,因此即使你正确地使用它们,它们看起来也会像错误一样。然而,在 Delphi 10.4 Sydney 中,Error Insight 通过引入语言协议(Language Protocol)进行了大修。语言服务器协议 (LSP) 的支持,使它们可以使。
内联变量允许你在需要时声明变量,而不是在过程的顶部。
让我们来看一段简短的代码片段:
Delphi
procedure TForm15.Button1Click(Sender: TObject);
var
Files: TArray<string>;
FileName: string;
begin
Files := TDirectory.GetFiles('c:\temp');
for FileName in Files do
ListBox1.Items.Add(FileName);
end;
在这里,您可以看到我们正在将 "c:\temp "文件夹中的所有文件列表到一个 TListBox 中。 我们可以将其改为内联声明变量。
像这样:
Delphi
procedure TForm15.Button2Click(Sender: TObject);
begin
var Files : TArray<string> := TDirectory.GetFiles('c:\temp');
for var FileName : string in Files do
ListBox1.Items.Add(FileName);
end;
现在,变量是在使用的地方声明的。 在这个简单的例子中没有什么区别,但当编写一个更大的方法时,你可以根据需要创建变量,而不必一直在方法的顶部声明它们。它们还具有局部作用域,因此 FileName 变量只在 for 循环的上下文中有效。
当我们添加类型推断(我们在上文的泛型中简单介绍过)时,我们可以将代码重写如下:
Delphi
procedure TForm15.Button3Click(Sender: TObject);
begin
var Files := TDirectory.GetFiles('c:\temp');
for var FileName in Files do
ListBox1.Items.Add(FileName);
end;
现在,编译器会根据上下文 "推断 "出类型,这样我们就能写出更简洁的代码。 更少的代码等于更少的类型,等于更高的生产率--至少理论上是这样。由于这两项功能都是 Delphi 的新功能,因此
要确定第三个示例是否比第一个示例 "更好",可能需要相当长的时间。它当然更简洁,看起来也不那么杂乱,但它是否更易于维护? 在现阶段,我还不能下定论。 我怀疑它可能会产生
一些难以发现的微妙错误(如 with 语句),但这还有待确定。