9.2 finally
代码块
还有第四个用于异常处理的关键字,我已经提到过,但到目前为止还没有使用过,那就是 finally
。finally
块用于执行一些应始终执行的操作(通常是清理操作)。事实上,无论是否发生异常,finally
代码块中的语句都会被处理。只有在未出现异常或出现异常并已处理的情况下,才会执行 try
代码块后面的普通代码。换句话说,即使出现异常,finally
代码块中的代码也总是在 try
代码块的代码之后执行。
请看这个方法(ExceptFinally
示例的一部分),它执行一些耗时的操作,并在窗体的标题中显示其状态:
procedure TForm1.BtnWrongClick(Sender: TObject);
var
I, J: Integer;
begin
Caption := 'Calculating';
J := 0;
// 长时间的(错误的)计算...
for I := 1000 downto 0 do
J := J + J div I;
Caption := 'Finished';
Show('Total: ' + J.ToString);
end;
由于算法中存在错误(因为变量 I 的值可以达到 0,而且还用于除法),程序会中断,但不会重置窗体标题。这就是 try-finally
块的作用:
procedure TForm1.BtnTryFinallyClick(Sender: TObject);
var
I, J: Integer;
begin
Caption := 'Calculating';
J := 0;
try
// 长时间的(错误的)计算...
for I := 1000 downto 0 do
J := J + J div I;
Show('Total: ' + J.ToString);
finally
Caption := 'Finished';
end;
end;
当程序执行该函数时,无论是否出现(任何类型的)异常,都会重置游标。此版本函数的缺点是无法处理异常。
9.2.1 finally
和 except
耐人寻味的是,在 Object Pascal
语言中,一个 try
代码块后面可以跟一个 Except
或 finally
语句,但不能同时跟这两个语句。鉴于您经常希望同时使用这两个块,典型的解决方案是使用两个嵌套的 try
块,内部块与 finally
语句相关联,外部块与 except 语句相关联,或者根据情况反之亦然。下面是 ExceptFinally
示例中第三个按钮的代码:
procedure TForm1.BtnTryTryClick(Sender: TObject);
var
I, J: Integer;
begin
Caption := 'Calculating';
J := 0;
try
try
// 长时间的(错误的)计算...
for I := 1000 downto 0 do
J := J + J div I;
Show('Total: ' + J.ToString);
except
on E: EDivByZero do
begin
// 使用新消息重新引发异常
raise Exception.Create('Error in Algorithm');
end;
end;
finally
Caption := 'Finished';
end;
end;
9.2.2 使用 finally
代码块恢复光标
try-finally
块的一个常见用例是资源的分配和释放。另一种相关情况是在操作完成后需要重置临时配置,即使该操作引发了异常。
需要还原的临时配置设置的一个例子是沙漏光标,它在长时间操作中显示,并在操作结束后还原为原来的光标。即使代码很简单,也总有可能出现异常,因此应始终使用 try-finally
块。
在 RestoreCursor
应用程序示例(一个 VCL
应用程序,因为 FireMonkey
中的光标管理比较复杂)中,我编写了以下代码,用于保存当前光标,将其临时设置为沙漏光标,并在操作结束时恢复原始光标:
var
CurrCur := Screen.Cursor;
Screen.Cursor := crHourGlass;
try
// 一些耗时操作
Sleep(5000);
finally
Screen.Cursor := CurrCur;
end;
9.2.3 使用托管记录恢复光标
要保护资源分配或定义要还原的临时配置,可以使用托管记录来代替显式的 try-finally 块,这需要编译器添加一个固有的 finally 块。这样,即使在定义记录时需要付出一些初始努力,保护资源或恢复配置的代码编写量也会减少。
这是一个托管记录,代表了上一节代码的相同行为,即在 Initialize 方法中将当前游标保存在一个字段中,并在 Finalize 方法中将其重置:
type
THourCursor = record
private
FCurrCur: TCursor;
public
class operator Initialize(out ADest: THourCursor);
class operator Finalize(var ADest: THourCursor);
end;
class operator THourCursor.Initialize(out ADest: THourCursor);
begin
ADest.FCurrCur := Screen.Cursor;
Screen.Cursor := crHourGlass;
end;
class operator THourCursor.Finalize(var ADest: THourCursor);
begin
Screen.Cursor := ADest.FCurrCur;
end;
一旦定义了这个托管记录:
var
HC: THourCursor;
// 一些耗时操作
Sleep(5000);
注解:你可以在 Erik van Bilsen 的以下博文中找到更多通过托管记录保护资源的例子: https://blog.grijjy.com/2020/08/03/automate-restorable-operations-with-custom-managed-records/。这是一系列关于托管记录的详细博客的一部分。