Perl 6 中允许失败

在本系列比较 Perl 5 和 Perl 6 的第八篇文章中,了解它们在创建和处理异常方面的差异。
201 位读者喜欢这篇文章。
hands programming

WOCinTech Chat。由 Opensource.com 修改。CC BY-SA 4.0

这是一系列文章中的第八篇,讲述了将代码从 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::TinySyntax::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 warningsno 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 模块时,您需要使用 evaltry 的某个版本来为可能的异常做好准备。在 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 ...

请注意,您不必提供 tryCATCHFailure 将从有问题的子例程/方法中返回,就好像一切正常一样。只有当 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"
    }
}

然后,你可以在代码中的任何 diefail 语句中使用该异常。

# 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 类。

标签
Elizabeth Mattijsen
自 1978 年以来,Elizabeth Mattijsen 一直在各种(现在大多已经过时)的编程语言中从事编程工作,然后她开始使用 Perl 4 进行编程。 1994 年,她创立了荷兰第一家商业网站开发公司,使用 Perl 5 作为主要编程语言。 从 2003 年开始,她参与了一项在线酒店预订服务的快速增长。

3 条评论

非常有用的帖子...感谢分享...

我喜欢 Python 的 try catch。 在 perl 6 中,很难理解发生了什么。 也许随着时间的推移,我会明白它是如何工作的。

Creative Commons License本作品采用 Creative Commons Attribution-Share Alike 4.0 International License 许可。
© . All rights reserved.