第七章. 当出错的时候

When things goes wrong

声明

本章翻译仅用于 Raku 学习和研究, 请支持电子版或纸质版

第七章. 当出错的时候

Raku doesn’t always immediately give up when something goes wrong. It can fail softly. If the result of that problem doesn’t affect anything else in the program there’s no need to complain about it. However, the moment it becomes a problem that passive failure demands your attention.

This chapter shows you the error mechanisms and how to deal with them. You’ll see how to handle the problems that your program notices on your behalf as well as detect and report problems on your own.

当出现问题时,Raku 并不总是立即放弃。它可能会轻轻失败。如果该问题的结果不影响程序中的任何其他内容,则无需抱怨它。然而,当它成为问题的那一刻被动失败就要引起你的注意了。

本章向你介绍错误机制以及如何处理它们。你将看到如何处理你的程序以你的名义注意到的问题,以及如何自行检测和报告问题。

异常

Here’s a bit of code that tries to convert nonnumeric text into a number. Maybe something else didn’t put the right value in the variable:

这里有一些代码试图将非数字文本转换为数字。也许其他东西没有在变量中放入正确的值:

my $m = 'Hello';
my $value = +$m;
put 'Hello there!';  # no error, so, works?

Your program doesn’t complain because you don’t do anything with the problematic result. Change the program to output the result of what you think was a numeric conversion:

你的程序没有抱怨,因为你没有对有问题的结果做任何事情。更改程序以输出你认为是数字转换的结果:

my $m = 'Hello';
my $value = +$m;
put $value;

Now you get some error output instead:

现在你得到一些错误输出:

Cannot convert string to number: base-10 number must
begin with valid digits or '.' in '⏏Hello' (indicated by ⏏)
    in block <unit> at ... line 2

Actually thrown at:
    in block <unit> at ... line 3

Look at that error message. It reports two line numbers. The error occurred on line 2, but it wasn’t until line 3 (the one with put) that it became a problem. That’s the soft failure. What’s actually in $result? It’s a Failure object:

看看那条错误信息。它报告两个行号。错误发生在第 2 行,但直到第 3 行(带有 put 的那行)才成为问题。那是软失败。 $result 中的实际内容是什么?这是一个 Failure 对象:

my $m = 'Hello';
my $value = +$m;
put "type is {$value.^name}";  # type is Failure

These soft failures can be quite handy in cases where you don’t care if something didn’t work. If you can’t log a message because the logger is broken what are you going to do about it? Log the failure? Similarly, sometimes you don’t care if something fails because that might be a common case. It’s up to you to make those decisions, though.

The Failure is really a wrapper around an Exception, so you need to know about exceptions first.

如果你不关心某些事情是否有效,这些软故障可能非常方便。如果由于记录器坏了而无法记录消息,你打算怎么做呢?记录失败?同样,有时你不关心某些事情是否失败,因为这可能是一种常见的情况。不过,由你决定做出决定。

Failure 实际上是Exception的包装,因此你首先需要了解异常。

捕获异常

Something that wants to report an exception throws it. You’d say “the subroutine threw an exception.” Some people might say it “raised an exception.” It’s the same thing. If you don’t handle an exception it stops your program.

A try wraps some code and can catch an Exception. If it catches an Exception it puts it into the $! special variable:

想要报告异常的东西会抛出它。你会说“子程序抛出了异常。” 有些人可能会说它“引发了异常。”这是同样的事情。如果你不处理异常,它将停止你的程序。

try 包装一些代码并可以捕获异常。如果它捕获一个 异常,它会将异常放入 $! 特殊变量:

try {
    my $m = 'Hello';
    my $value = +$m;
    put "value is {$value.^name}";
}
put "ERROR: $!" if $!;

put 'Got to the end.';

You catch the Exception and your program continues:

你捕获异常并你的程序继续:

ERROR: Cannot convert string to number
Got to the end.

With a single line you don’t need the braces:

使用单行你不需要大括号:

my $m = 'Hello';
try my $value = +$m;

If the code succeeds try gives back the value. You can move the try to the other side of the assignment:

如果代码成功,则 try 返回值。你可以将 try 移动到赋值的另一侧:

my $m = 'Hello';
my $value = try +$m;

Most Exception types are under X and inherit from Exception—in this example it’sX::Str::Numeric:

大多数 Exception 类型都在 X 下,并且从 Exception 继承 - 在这个例子中,它是 X::Str::Numeric

put "Exception type is {$!.^name}";

If there was no Exception, the thingy in $! is Any. This is a bit annoying because Exception inherits from Any too. The $! in a condition is defined if there was a problem. Use it with given and smart match against the types you want to handle. A default Block handles anything you don’t:

如果没有 Exception$! 里面的东西就是 Any。这有点烦人,因为 Exception 也继承了 Any。 如果出现问题,$! 就处于定义状态。使用 given 和智能匹配你要处理的类型。default Block 处理你不需要的任何内容:

put 'Problem was ', do given $! {
    when X::Str::Numeric { ... }
    default { ... }
};

Exception types use different methods to give you more information. Each type defines a method that makes sense for its error. Look at each type to see which information it captures for you. The X::Str::NumericException type knows at which position in the Str it discovered the problem:

Exception类型使用不同的方法为你提供更多信息。每种类型都定义了一种对其错误有意义的方法。查看每种类型以查看它为你捕获的信息。 X::Str::Numeric Exception类型知道它在字符串中发现问题的位置:

put 'Problem was ', do given $! {
    when X::Str::Numeric  { "Char at {.pos} is not numeric" }
    when X::Numeric::Real { "Trying to convert to {.target}" }
    default { ... }
};

The $! and given happen outside of the try. A CATCH Block inside the try can do the same thing. The X::Str::Numeric Exception shows up in $_:

$!given 发生在 try 之外。 try 中的 CATCH 可以做同样的事情。 X::Str::Numeric Exception 出现在 $_ 中:

try {
    CATCH {
        when X::Str::Numeric { put "ERROR: {.reason}" }
        default { put "Caught {.^name}" }
        }
    my $m = 'Hello';
    my $value = +$m;
    put "value is {$value.^name}";
    }

put 'Got to the end.';

The X::Str::Numeric Exception is thrown at the line my $value = +$m;, then it skips the rest of the Block. Handle this by outputting an error and continuing with the program:

X::Str::Numeric Exceptionmy $value =+ $m; 这一行被抛出,然后跳过的其余部分。通过输出错误并继续执行程序来处理:

ERROR: base-10 number must begin with valid digits or '.'
Got to the end.

Most of these objects inherit from Exception and have a .message method that provides more information. Catch those with default where you can output the name of the type:

这些对象中的大多数都继承自Exception,并且具有提供更多信息的 .message 方法。使用 default 捕获那些异常,你可以输出类型的名称:

try {
    CATCH {
        default { put "Caught {.^name} with 「{.message}」" }
        }
    my $m = 'Hello';
    my $value = +$m;
    put "value is {$value.^name}";
    }

put "Got to the end.";

Now the output has the same message that the unhandled error showed you:

现在输出与未处理的错误显示的消息相同:

Caught X::Str::Numeric with 「Cannot convert string to number:
base-10 number must begin with valid digits or '.' in '⏏Hello'
(indicated by ⏏)」
Got to the end.

EXERCISE 7.1Divide a number by zero. What Exception type do you get?

练习7.1 将数字除以零。你得到什么 Exception 类型?

Backtraces

The Exception contains a Backtrace object that documents the path of the error. This example has three levels of subroutine calls with some code at the end that throws an Exception:

Exception 包含一个 Backtrace 对象,用于记录错误的路径。此示例有三个级别的子例程调用,最后的一些代码抛出异常

sub top    { middle()    }
sub middle { bottom()    }
sub bottom { 5 + "Hello" }

top();

You don’t handle the Exception in middle, you don’t handle it in top, and finally, you don’t handle it at the top level. The Exception complains and shows you its path through the code:

你不在 middle 中处理异常,你不在顶部处理它,最后,你不在 top 中处理它。 Exception 会抱怨并向你展示其代码的路径:

Cannot convert string to number: base-10 number must
begin with valid digits or '.' in '⏏Hello' (indicated by ⏏)
  in sub bottom at backtrace.p6 line 3
  in sub middle at backtrace.p6 line 2
  in sub top at backtrace.p6 line 1
  in block <unit> at backtrace.p6 line 5

Actually thrown at:
  in sub bottom at backtrace.p6 line 3
  in sub middle at backtrace.p6 line 2
  in sub top at backtrace.p6 line 1
  in block <unit> at backtrace.p6 line 5

Don’t fret when you see long messages like this. Find the first level of the error and think about that. Work your way through the chain one level at a time. Often a simple fix makes it all go away.

You can handle the Exception anywhere in that chain. The simplest option might be to wrap the call to bottom in a try. With a single-line expression you can omit the Block around the code. In middle, you don’t specify a CATCH so there’s a default handler that discards the Exception:

当你看到像这样的长信息时,不要担心。找出错误的第一级并考虑一下。一次一个层次地通过链条。通常一个简单的修复使它全部消失。

你可以在该链中的任何位置处理异常。最简单的选择可能是在 try 中将调用包装到 bottom。使用单行表达式,你可以省略代码周围的。在 middle 中,你没有指定 CATCH,因此有一个默认处理程序可以丢弃Exception

sub top    { middle()      }
sub middle { try bottom()  }
sub bottom { 137 + 'Hello' }

put top();

That program doesn’t produce an error (or any output). That’s probably not what you want. The middle layer can handle the case where you can’t convert a Str to a number by returning the special number NaN (for “not a number”):

该程序不会产生错误(或任何输出)。这可能不是你想要的。中间层可以通过返回特殊数字 NaN (意思是“非数字”)来处理无法将字符串转换为数字的情况:

sub top    { middle() }
sub middle {
    try {
        CATCH { when X::Str::Numeric { return NaN } }
        bottom()
        }
}

sub bottom { 137 + 'Hello' }

put top();

Change the code to make a different error that the CATCH in middle doesn’t handle. Try to divide by zero and convert the result to a Str:

更改代码以产生 middle 中的 CATCH 无法处理的其他错误。尝试除以零并将结果转换为字符串

sub top    { middle() }
sub middle {
    try {
        CATCH { when X::Str::Numeric { return NaN } }
        bottom()
        }
}

sub bottom { ( 137 / 0 ).Str  }

put top();

The CATCH in middle doesn’t handle this new type of error, so the Exception interrupts the program at the call to top:

middle 中的 CATCH 不处理这种新类型的错误,因此Exception在调用 top 时中断程序:

Attempt to divide 137 by zero using div
  in sub bottom at nan.p6 line 12
  in sub middle at nan.p6 line 8
  in sub top at nan.p6 line 4
  in block <unit> at nan.p6 line 14

Catch this one inside top. The Exception passes through middle, which has nothing to handle it. Since nothing handles that error it continues up the chain and ends up in top, which handles it by returning the special value Inf (for infinity):

top 里面捕获这个异常。Exception通过 middle,没有任何处理它。由于没有处理该错误,它会继续向上链接并最终到达 top,通过返回特殊值 Inf (意思是无穷大)来处理它:

sub top {
    try {
        CATCH {
            when X::Numeric::DivideByZero { return Inf }
        }
        middle()
    }
}
sub middle {
    try {
        CATCH {
            when X::Str::Numeric { return NaN }
        }
        bottom()
    }
}

sub bottom { ( 137 / 0 ).Str  }

put top();

Extend this process as far up the chain as you like. The next example changes the error by trying to call an undefined method on 137:

根据你的喜好将这个过程延伸到链上。下一个示例通过尝试在 137 上调用未定义的方法来更改错误:

sub top {
    try {
        CATCH {
            when X::Numeric::DivideByZero { return Inf }
        }
        middle()
    }
}

sub middle {
    try {
        CATCH {
            when X::Str::Numeric { return NaN }
        }
        bottom()
    }
}

sub bottom { 137.unknown-method }

try {
    CATCH {
        default { put "Uncaught exception {.^name}" }
    }
    top();
}

That’s a new sort of error that you don’t handle so far:

到目前为止,这是一种新的错误:

Uncaught exception X::Method::NotFound

Sometimes you don’t care about unfound methods. If it’s there you call it and if not you want to ignore it. There’s special syntax for this. If you place a ? after the method call dot, you don’t get an Exception if the method is not found:

有时你不关心未找到的方法。如果它在那里你就调用它,如果不是你就忽略它。这有特殊的语法。如果在方法调用点之后你放一个 ?,如果找不到该方法,则不会出现异常

sub bottom { 137.?unknown-method }

Rethrowing Errors

It gets better. You can catch an exception but not handle it. Modify the CATCH in middle to intercept X::Method::NotFound and output a message, then .rethrow it:

它变得更好了。你可以捕获异常但不处理它。修改 middle 里面的 CATCH 以拦截 X::Method::NotFound 并输出一条消息,然后 .rethrow 它:

sub top    {
    try {
        CATCH {
            when X::Numeric::DivideByZero { return Inf }
        }
        middle()
    }
}

sub middle {
    try {
        CATCH {
            when X::Str::Numeric     { return NaN }
            when X::Method::NotFound {
                put "What happened?";
                .rethrow
            }
        }
        bottom()
    }
}

sub bottom { 137.unknown-method  }

try {
    CATCH {
        default { put "Uncaught exception {.^name}" }
    }
    top();
}

You can see that middle was able to do its work but the Exception was ultimately handled by top:

你可以看到 middle 能够完成它的工作,但是Exception最终由 top 处理:

What happened?
Uncaught exception X::Method::NotFound

EXERCISE 7.2Implement a subroutine whose only code is .... That denotes code that you intend to fill in later. Call that from another subroutine and catch the Exception. What’s the type you get? Can you output your own Backtrace?

练习7.2 实现一个子程序,其唯一的代码是 ... 。这表示你打算稍后填写的代码。从另一个子例程调用它并捕获异常。你得到的是什么类型的?你能输出自己的Backtrace吗?

Throwing Your Own Exceptions

Up to now you’ve seen exceptions that come from problems in the source code. Those are easy to see without complicating the examples. You can also throw your own Exceptions. The easiest way is to use die with a Str argument:

到目前为止,你已经看到源代码中存在问题的异常。这些很容易看到,而不会使示例复杂化。你也可以抛出自己的Exception。最简单的方法是使用带有字符串参数的 die

die 'Something went wrong!';

The die subroutine takes the Str as the message for an Exception of type X::AdHoc. That’s a catch-all type for anything that doesn’t have a more appropriate type.

Dying with a Str is the same as constructing an X::AdHoc. You can die with a particular Exception type by constructing it yourself:

die 子例程将字符串作为 X::AdHoc 类型的异常的消息。对于没有更合适类型的任何东西来说,这是一种全能类型。

带有字符串的死亡与构建X::AdHoc相同。你可以通过自己构建它来使用特定的Exception类型:

die X::AdHoc.new( payload => "Something went wrong!" );
NOTE

You’re actually creating a Pair here, but you won’t see the => until Chapter 9 or named parameters until Chapter 11, so take this on faith.

The die is important. Merely constructing the Exception does not throw it:

你实际上是在这里创建一个Pair,但在第11章之前你不会看到 => 或者直到第11章,所以请相信这一点。

die 很重要。仅仅构建Exception并不会抛出它:

# nothing happens
X::AdHoc.new( payload => "Something went wrong!" );

You can .throw it yourself if you like, though. This is the same as die:

不过,如果你愿意,你可以自己 .throw。这跟 die 是一样的:

X::AdHoc
    .new( payload => "Something went wrong!" )
    .throw;

You can also create Exceptions of other predefined types. The X::NYI type is for features not yetimplemented:

你还可以创建其他预定义类型的 ExceptionX::NYI 类型用于未实现的功能:

X::NYI.new: features => 'Something I haven't done yet';

EXERCISE 7.3Modify the previous exercise to die with a Str argument. What type do you catch? Further modify that to die with an X::StubCode object that you construct yourself.

练习7.3 使用字符串参数修改上一个练习。你捕获了什么类型?使用你自己构建的 X::StubCode 对象进一步修改它。

Defining Your Own Exception Types

It’s a bit early to create subclasses—you’ll see how to do that in Chapter 12—but with a little faith you can do this right away. Base your class on Exception without doing anything else:

创建子类有点早 - 你会在第12章看到如何做到这一点 - 但有一点信心你可以马上做到这一点。将你的类基于Exception而不做任何其他事情:

class X::MyException is Exception {}

sub my-own-error {
    die X::MyException.new: payload => 'I did this';
}

my-own-error();

When you run the my-own-error subroutine it dies with the new error type that you’ve defined:

当你运行 my-own-error 子例程时,它会以你定义的新错误类型失败:

Died with X::MyException

Now that your new type exists you can use it in a CATCH (or smart match). Even without any sort of customization its name is enough to tell you what happened:

既然你的新类型存在,你可以在 CATCH (或智能匹配)中使用它。即使没有任何自定义,它的名字也足以告诉你发生了什么:

try {
    CATCH {
        when X::MyException { put 'Caught a custom error' }
    }

my-own-error();
}

Chapter 12 will cover class creation and show you more about what you can do inside a class and how to reuse existing classes.

第12章将涵盖类的创建,并向你展示更多关于你可以在类中做什么以及如何重用现有类。

Failures

Failures are wrappers around unthrown Exceptions. They’re passive until you try to use them later—hence “soft” exceptions. They don’t interrupt your program until something tries to use them as a normal value. It’s then that they throw their Exceptions.

As a Boolean a Failure is always False. You can “disarm” the Failure by checking it. That might be with an if, with a logical test, or by Booleanizing it with .so or ?. All of those mark the Failure as handled and prevent it from implicitly throwing its Exception:

Failure 是围绕unthrown Exception 的包装器。它们是被动的,直到你以后尝试使用它们 - 因此是“软”异常。在尝试将它们用作正常值之前,它们不会中断你的程序。然后,他们抛出了他们的异常。

作为布尔值,Failure 始终为 False。你可以通过检查来“解除”Failure。这可能是使用 if,进行逻辑测试,或者使用 .so? 进行布尔化。所有这些都将Failure标记为已处理并阻止它隐式抛出其异常:

my $result = do-something();
if $result { ... }
my $did-it-work = ?$results;

A Failure is always undefined; maybe you want to set a default value if you encounter one:

Failure 总是未定义的;也许你想要设置一个默认值,如果你遇到一个:

my $other-result = $result // 0;

You can handle a Failure yourself without the try. The .exception method extracts that object so you can inspect it:

没有 try 你也可以自己处理Failure.exception 方法提取该对象,以便你可以检查它:

unless $result {
    given $result.exception {
        when    X::AdHoc { ... }
        default          { ... }
    }
}

Create your own Failures by substituting die with fail:

通过将 die 替换为 fail 来创建自己的Failure

fail "This ends up as an X::AdHoc";

fail My::X::SomeException.new(
    :payload( 'Something wonderful' ) );

When you use fail in a subroutine the Failure object becomes the return value. Instead of die-ing you should probably use fail so that the programmers who use your code can decide for themselves how to handle the problem.

EXERCISE 7.4Create a subroutine that takes two arguments and returns their sum. If either argument is not a number return a Failure object saying so. How would you handle the Failure?

在子例程中使用 fail 时,Failure 对象将成为返回值。你应该使用 fail 而不是 die,以便使用你的代码的程序员可以自己决定如何处理问题。

练习7.4 创建一个带有两个参数并返回其总和的子程序。如果任一参数不是数字,则返回Failure对象。你会如何处理Failure

Warnings

Instead of die-ing you can use warn to give the same message without stopping the program or forcing the program to catch the Exception:

你可以使用 warn 而不是 die 来提供相同的消息,而无需停止程序或强制程序捕获异常:

warn 'Something funny is going on!';

Warnings are a type of Exception and you can catch them. They aren’t the same type of exception so they don’t show up in a CATCH Block. They are control exceptions that you catch in a CONTROL Block:

警告是一种异常,你可以捕获它们。它们不是同一类型的异常,因此它们不会出现在 CATCH 中。它们是你在CONTROL 中捕获的控制异常:

try {
    CONTROL {
        put "Caught an exception, in the try";
        put .^name;
    }
    do-that-thing-you-do();
}

sub do-that-thing-you-do {
    CONTROL {
        put "Caught an exception, in the sub";
        put .^name;
    }
    warn "This is a warning";
}

If you don’t care about the warnings (they are annoying after all) you can wrap the annoying code in a quietly Block:

如果你不关心警告(毕竟它们很烦人),你可以将烦人的代码包装在一个 quietly 中:

quietly {
    do-that-thing-you-do();
    }

EXERCISE 7.5Modify the previous exercise to warn for each argument that cannot be converted to a number. Once you’ve seen those warnings, further modify the program to ignore the warnings.

练习7.5 修改上一个练习,以警告(warn)每个无法转换为数字的参数。一旦看到这些警告,进一步修改程序以忽略警告。

The Wisdom of Exceptions

Exceptions can be a contentious subject. Some people love them and some hate them. Since they are a feature, you need to know about them. I want to leave you with some words of caution before you get yourself in too much trouble. Your love/hate relationship with Exceptions will most likely fluctuate over your career.

Exceptions are a way of communicating information. By design this feature expects you to recognize the type of error and handle it appropriately. This implies that you can actually handle the error. If you encounter a situation that your program cannot correct, an Exception might not be the appropriate feature to use.

Even if your program could correct the error, many people don’t expect most programmers to handle errors. Your Exception may be a nuisance that they catch and ignore. Think about that before you spend too much time crafting fine-grained Exception types that cover all situations.

As part of program flow Exceptions are really a fancy break mechanism. You’re in one bit of code, then suddenly in another. Those cases should truly be exceptional and rare. Anything else that you expect to happen you should handle with normal program flow.

That’s all I’ll say about that. Perhaps you have a different opinion. That’s fine. Read more about this on your own and judge your particular situation.

Exception 可能是一个有争议的主题。有些人喜欢他们,有些人讨厌他们。由于它们是一个功能,你需要了解它们。在你遇到太多麻烦之前,我想给你一些谨慎的话。你与异常的爱/恨关系很可能会在你的职业生涯中波动。

异常是一种沟通信息的方式。根据设计,此功能要求你识别错误类型并正确地处理它。这意味着你可以正确地处理错误。如果遇到程序无法纠正的情况,则异常这个功能可能不适用。

即使你的程序可以纠正错误,许多人也不希望大多数程序员处理错误。你的异常可能是他们捕获和忽视的麻烦。在花费太多时间制作涵盖所有情况的细粒度异常类型之前,请考虑一下。

作为程序流程的一部分,异常实际上是一种奇特的破坏机制。你在一点代码中,然后突然在另一个代码中。这些情况应该是特殊和罕见的。你期望发生的任何其他事情都应该处理正常的程序流程。

这就是我要说的全部内容。也许你有不同的意见。没关系。自己阅读更多相关信息并判断你的具体情况。

Summary

Exceptions are a feature of Raku, but it doesn’t hit you over the head with them. They can be soft failures until they would actually cause a problem.

Exception是 Raku 的一个功能,但它并没有让你头脑发热。它们可能是软故障,直到它们实际上会导致问题。

Don’t become overwhelmed by the different types of Exceptions your program may report. You’ll continue to see these throughout the rest of the book. Use them appropriately (whatever definition you choose) and effectively to note problems in your program. Try to detect those as early as you can.

不要被你的程序可能报告的不同类型的异常所淹没。在本书的其余部分中,你将继续看到这些内容。适当地使用它们(无论你选择何种定义)并有效地记录程序中的问题。尽可能早地检测这些。

comments powered by Disqus