这是关于将代码从 Perl 5 迁移到 Perl 6 的系列文章中的第八篇。本文着眼于 Perl 5 和 Perl 6 在创建和处理异常方面的差异。
本文的第一部分描述了在 Perl 6 中使用异常,第二部分解释了如何创建您自己的异常,以及失败确实是 Perl 6 中的一种选择。
异常处理阶段器
在 Perl 5 中,您可以使用 eval 来捕获一段代码中的异常。在 Perl 6 中,此功能由 try 覆盖
# Perl 5
eval {
die "Goodbye cruel world";
};
say $@; # Goodbye cruel world at ...
say "Alive again!"
# Perl 6
try {
die "Goodbye cruel world";
}
say $!; # Goodbye cruel world in block ...
say "Alive again!"
在 Perl 5 中,您也可以在表达式中使用 eval 的返回值
# Perl 5
my $foo = eval { ... }; # undef if exception was thrown
这在 Perl 6 中对于 try 来说也是一样的工作方式
# Perl 6
my $foo = try { 42 / $something }; # Nil if $something is 0
甚至不必是一个代码块
# Perl 6
my $foo = try 42 / $something; # Nil if $something is 0
在 Perl 5 中,如果您需要更精细地控制异常发生时要执行的操作,您可以使用特殊的 信号处理程序 $SIG{__DIE__} 和 $SIG{__WARN__}。
在 Perl 6 中,这些被两个异常处理阶段器取代,由于它们的作用域行为,必须始终使用花括号指定。这些异常处理阶段器(在下表中)仅适用于周围的代码块,并且您在一个代码块中只能拥有每种类型一个。
名称 | 描述 |
---|---|
CATCH | 当抛出异常时运行 |
CONTROL | 为任何(其他)控制异常运行 |
捕获异常
Perl 5 中的 $SIG{__DIE__} 伪信号处理程序不再推荐使用。有几个竞争的 CPAN 模块提供了 try/catch 机制(例如:Try::Tiny 和 Syntax::Keyword::Try)。尽管这些模块在实现上完全不同,但它们提供了非常相似的语法,只有非常细微的语义差异,因此它们是比较 Perl 6 和 Perl 5 功能的好方法。
在 Perl 5 中,您只能结合 try 代码块捕获异常
# Perl 5
use Try::Tiny; # or Syntax::Keyword::Try
try {
die "foo";
}
catch {
warn "caught error: $_"; # $@ when using Syntax::Keyword::Try
}
Perl 6 不需要 try 代码块。无论何时在紧邻的词法作用域中抛出异常,都会调用 CATCH 阶段器中的代码
# Perl 6
{ # surrounding scope, added for clarity
CATCH {
say "aw, died";
.resume; # $_, AKA the topic, contains the exception
}
die "goodbye cruel world";
say "alive again";
}
# aw, died
# alive again
同样,您不需要 try 语句来捕获 Perl 6 中的异常。如果您愿意,您可以单独使用 try 代码块,但这只是一种方便的方式来忽略在该代码块中抛出的任何异常。
另请注意,在 CATCH 代码块内,$_ 将设置为 Exception 对象。在本例中,执行将继续执行导致 Exception 抛出的语句之后的语句。这是通过调用 Exception 对象上的 resume 方法来实现的。如果异常未恢复,它将再次抛出,并可能被外部 CATCH 代码块捕获(如果存在)。如果不存在外部 CATCH 代码块,则异常将导致程序终止。
when 语句使检查特定异常变得容易
# Perl 6
{
CATCH {
when X::NYI { # Not Yet Implemented exception thrown
say "aw, too early in history";
.resume;
}
default {
say "WAT?";
.rethrow; # throw the exception again
}
}
X::NYI.new(feature => "Frobnicator").throw; # caught, resumed
now / 0; # caught, rethrown
say "back to the future";
}
# aw, too early in history
# WAT?
# Attempt to divide 1234.5678 by zero using /
在本例中,只有 X::NYI 异常会恢复;所有其他异常都将抛给任何外部 CATCH 代码块,并可能导致程序终止。我们将永远无法回到未来。
捕获警告
如果您不希望在执行一段代码时发出任何警告,您可以在 Perl 5 中使用 no warnings 编译指示
# Perl 5
use warnings; # need to enable warnings explicitely
{
no warnings;
my $foo;
say $foo; # no visible warning
}
my $bar;
print $bar;
# Use of uninitialized value $bar in print...
在 Perl 6 中,您可以使用 quietly 代码块
# Perl 6
# warnings are enabled by default
quietly {
my $foo;
say $foo; # no visible warning
}
my $bar;
print $bar;
# Use of uninitialized value of type Any in string context...
quietly 代码块将捕获并忽略从该代码块发出的任何警告。
如果您想更精细地控制您想看到的警告,您可以使用 use warnings 或 no warnings 分别在 Perl 5 中选择您要启用或禁用的 警告类别。例如
# Perl 5
use warnings;
{
no warnings 'uninitialized';
my $bar;
print $bar; # no visible warning
}
如果您想在 Perl 6 中进行更精细的控制,您将需要一个 CONTROL 阶段器。
CONTROL
CONTROL 阶段器非常像 CATCH 阶段器,但它处理一种特殊的异常类型,称为“控制异常”。每当在 Perl 6 中生成警告时,都会抛出控制异常,您可以使用 CONTROL 阶段器捕获它。此示例将不显示在表达式中使用未初始化值的警告
# Perl 6
{
CONTROL {
when CX::Warn { # Control eXception type for Warnings
note .message
unless .message.starts-with('Use of uninitialized value');
}
}
my $bar;
print $bar; # no visible warning
}
Perl 6 中目前没有定义警告类别,但正在讨论用于未来的开发。在此期间,您将必须检查控制异常 CX::Warn 类型的实际message,如上所示。
除了警告之外,控制异常机制还用于相当多的其他功能。以下语句(按字母顺序排列)也会创建控制异常
由这些语句生成的控制异常也会显示在任何 CONTROL 阶段器中。幸运的是,如果您不对给定的控制异常执行任何操作,它将在 CONTROL 阶段器完成时重新抛出,并确保执行其预期的操作。
失败也是一种选择
在 Perl 5 中,当使用 CPAN 模块时,您需要通过使用 eval 或 try 的某种版本来为可能的异常做好准备。在 Perl 6 中,您可以使用 try 执行相同的操作(如前所述)。
但是 Perl 6 还有另一个选项:Failure,这是一个用于包装 Exception 的特殊类。每当以意外的方式使用 Failure 对象时,它将抛出它包装的 Exception。这是一个简单的例子
# Perl 6
my $handle = open "non-existing file";
say "we tried to open the file";
say $handle.get; # unanticipated use of $handle, throws exception
say "this will never be shown";
# we tried to open the handle
# Failed to open file non-existing file: No such file or directory
如果成功打开请求的文件,Perl 6 中的 open 函数会返回一个 IO::Handle。如果失败,它会返回一个 Failure。然而,这不是抛出异常的原因——如果我们实际上尝试以意外的方式使用 Failure,那么 Exception 将会被抛出。
只有两种方法可以阻止 Failure 内部的 Exception 被抛出(即,预测潜在的失败)
- 在 Failure 上调用 .defined 方法
- 在 Failure 上调用 .Bool 方法
在这两种情况下,这些方法都将返回 False(即使从技术上讲 Failure 对象已实例化)。除此之外,它们还将 Failure 标记为“已处理”,这意味着如果稍后以意外的方式使用 Failure,它将不会抛出 Exception,而只是返回 False。
在大多数其他实例化对象上调用 .defined 或 .Bool 始终会返回 True。这为您提供了一种简单的方法来找出您期望返回“真实”实例化对象的内容是否返回了您可以真正使用的内容。
然而,这看起来像很多工作。幸运的是,您不必显式调用这些方法(除非您真的想这样做)。让我们重新措辞上面的代码,以更温和地处理无法打开文件的情况
# Perl 6
my $handle = open "non-existing file";
say "tried to open the file";
if $handle { # "if" calls .Bool, True on an IO::Handle
say "we opened the file";
.say for $handle.lines; # read/show all lines one by one
}
else { # opening the file failed
say "could not open file";
}
say "but still in business";
# tried to open the file
# could not open file
# but still in business
抛出异常
与 Perl 5 中一样,创建和抛出异常的最简单方法是使用 die 函数。在 Perl 6 中,这是创建 X::AdHoc Exception 并抛出它的快捷方式
# Perl 5
sub alas {
die "Goodbye cruel world";
say "this will not be shown";
}
alas;
# Goodbye cruel world at ...
# Perl 6
sub alas {
die "Goodbye cruel world";
say "this will not be shown";
}
# Goodbye cruel world
# in sub alas at ...
# in ...
Perl 5 和 Perl 6 中的 die 之间存在一些细微的差异,但语义上它们是相同的:它们立即停止执行。
返回失败
Perl 6 添加了 fail 函数。这将立即从周围的子例程/方法返回给定的 Exception:如果为 `fail` 函数提供了一个字符串(而不是一个 `Exception` 对象),则将创建一个 X::AdHoc 异常。
假设您有一个子例程,它接受一个参数,该参数被检查真值
# Perl 6
sub maybe-alas($expected) {
fail "Not what was expected" unless $expected;
return 42;
}
my $a = maybe-alas(666);
my $b = maybe-alas("");
say "values gathered";
say $a; # ok
say $b; # will throw, because it has a Failure
say "still in business"; # will not be shown
# values gathered
# 42
# Not what was expected
# in sub maybe-alas at ...
请注意,您不必提供 try 或 CATCH:Failure 将从相关的子例程/方法返回,就像一切正常一样。只有当以意外的方式使用 Failure 时,才会抛出嵌入其中的 Exception。处理此问题的另一种方法是
# Perl 6
sub maybe-alas($expected) {
fail "Not what was expected" unless $expected;
return 42;
}
my $a = maybe-alas(666);
my $b = maybe-alas("");
say "values gathered";
say $a ?? "got $a for a" !! "no valid value returned for a";
say $b ?? "got $b for b" !! "no valid value returned for b";
say "still in business";
# values gathered
# got 42 for a
# no valid value returned for b
# still in business
请注意,三元运算符 ?? !! 在条件上调用 .Bool,因此它有效地解除了 fail 返回的 Failure 的武装。
您可以将 fail 视为返回 Failure 对象的语法糖
# Perl 6
fail "Not what was expected";
# Perl 6
return Failure.new("Not what was expected"); # semantically the same
创建您自己的异常
Perl 6 使创建您自己的(类型化的)Exception 类非常容易。您只需要从 Exception 类继承并提供一个 message 方法。习惯上在 X:: 命名空间中创建自定义类。例如
# Perl 6
class X::Frobnication::Failed is Exception {
has $.reason; # public attribute
method message() {
"Frobnication failed because of $.reason"
}
}
然后,您可以在任何 die 或 fail 语句中使用该异常
# Perl 6
die X::Frobnication::Failed.new( reason => "too much interference" );
# Perl 6
fail X::Frobnication::Failed.new( reason => "too much interference" );
您可以在 CATCH 代码块中检查该异常,并在必要时进行内省
# Perl 6
CATCH {
when X::Frobnicate::Failed {
if .reason eq 'too much interference' {
.resume # too much interference is ok
}
}
} # all others will re-throw
您可以完全自由地设置您的 Exception 类;该类唯一需要提供的是一个名为“message”的方法,该方法应返回一个字符串。该字符串的创建方式完全取决于您,只要 method 返回一个字符串即可。如果您喜欢使用错误代码,您可以
# Perl 6
my @texts =
"unknown error",
"too much interference",
;
my constant TOO_MUCH_INTERFERENCE = 1;
class X::Frobnication::Failed is Exception {
has Int $.reason = 0;
method message() {
"Frobnication failed because of @texts[$.reason]"
}
}
如您所见,这很快变得更加复杂,因此您的效果可能会有所不同。
总结
在 Perl 6 中,捕获异常和警告由阶段器处理,而不是像 Perl 5 中那样由 eval 或信号处理程序处理。Exception 在 Perl 6 中是一等公民。
Perl 6 还引入了 Failure 对象的概念,该对象嵌入了一个 Exception 对象。如果以意外的方式使用 Failure 对象,则将抛出嵌入的 Exception。
您可以使用 if、?? !!(通过调用 .Bool 方法检查真值)和 with(通过调用 .defined 方法检查定义性)轻松检查 Failure。
您还可以通过从 Exception 类继承并提供 message 方法来非常轻松地创建 Exception 类。
3 条评论