这是一系列文章中的第八篇,讲述了将代码从 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
同样,您不需要在 Perl 6 中使用 try 语句来捕获异常。如果您愿意,可以单独使用 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 块将捕获并忽略从该块发出的任何警告。
如果您想更精细地控制想要看到的警告,可以使用 Perl 5 中的 use warnings 或 no warnings 来选择要启用或禁用的 警告类别。例如
# 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' 的方法,该方法应返回一个字符串。 如何创建该字符串完全取决于你,只要这个方法 返回一个字符串即可。 如果你更喜欢使用错误代码,你可以:
# 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 中的 phaser 处理,而不是像 Perl 5 中那样通过 eval 或信号处理程序处理。Exception 在 Perl 6 中是一流的对象。
Perl 6 还引入了 Failure 对象的概念,它嵌入了一个 Exception 对象。 如果 Failure 对象以一种意想不到的方式使用,则嵌入的 Exception 将会被抛出。
你可以使用 if、?? !! (它通过调用 .Bool 方法来检查真值)和 with (它通过调用 .defined 方法来检查定义性)来轻松检查 Failure。
你还可以通过从 Exception 类继承并提供一个 message 方法来轻松创建 Exception 类。
3 条评论