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

同样,您需要 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 warningsno 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 模块时,您需要通过使用 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”的方法,该方法应返回一个字符串。该字符串的创建方式完全取决于您,只要 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 类。

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

3 条评论

信息量很大的帖子...谢谢分享...

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

Creative Commons License本作品根据知识共享署名-相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.