需要结束可以正常死亡的词法范围动作

时间:2011-01-05 23:49:51

标签: perl exception lexical

我需要能够在动作可能会死的词块中添加动作。而且我需要正常抛出异常并且能够被正常捕获。

不幸的是,在DESTROY期间Perl特殊情况下的例外情况都是通过在消息中添加“(清理)”并使它们无法覆盖。例如:

{
    package Guard;

    use strict;
    use warnings;

    sub new {
        my $class = shift;
        my $code = shift;
        return bless $code, $class;
    }

    sub DESTROY {
        my $self = shift;
        $self->();
    }
}

use Test::More tests => 2;

my $guard_triggered = 0;

ok !eval {
    my $guard = Guard->new(
#line 24
        sub {
            $guard_triggered++;
            die "En guarde!"
        }
    );
    1;
}, "the guard died";

is $@, "En guarde! at $@ line 24\n",    "with the right error message";
is $guard_triggered, 1,                 "the guard worked";

我想要通过。目前,eval完全吞下了这个例外。

这适用于Test :: Builder2,所以除了纯Perl之外我不能使用任何东西。

根本问题是我有这样的代码:

{
    $self->setup;

    $user_code->();

    $self->cleanup;
}

即使$ user_code死掉也必须进行清理,否则$ self会进入奇怪的状态。所以我这样做了:

{
    $self->setup;

    my $guard = Guard->new(sub { $self->cleanup });

    $user_code->();
}

复杂性的出现是因为清理运行任意用户代码,并且它是一个用例,代码将会死亡。我希望这个例外可以被警卫所取代,并且不会改变。

由于改变堆栈的方式,我正避免将所有内容包装在eval块中。

2 个答案:

答案 0 :(得分:2)

这在语义上是否合理?根据我的理解,你有这个(伪代码):

try {
    user_code(); # might throw
}
finally {
    clean_up(); # might throw
}

有两种可能性:

  • user_code()clean_up()永远不会投入相同的运行,在这种情况下,您可以将其编写为顺序代码而不会有任何有趣的后卫业务,并且它将起作用。
  • user_code()clean_up()可能会在某个时刻同时进行。

如果两个函数都可能抛出,那么您有两个活动异常。我不知道任何语言可以处理多个当前抛出的活动异常,我确信这是有充分理由的。 Perl添加(in cleanup)并使异常无法应用; C++ calls terminate()Java drops the original exception silently等等。

如果您刚刚eval user_code()cleanup()引发了异常,那么您希望在$@中找到什么?

通常这表示您需要在本地处理清理异常,可能忽略清理异常:

try {
    user_code();
}
finally {
    try {
        clean_up();
    }
    catch {
        # handle exception locally, cannot propagate further
    }
}

或者你需要选择一个异常以便在两次抛出时忽略(这是DVK的解决方案所做的;它忽略了user_code()异常):

try {
    user_code();
}
catch {
    $user_except = $@;
}
try {
    cleanup();
}
catch {
    $cleanup_except = $@;
}
die $cleanup_except if $cleanup_except; # if both threw, this takes precedence
die $user_except if $user_except;

或以某种方式将两个异常组合成一个异常对象:

try {
    user_code();
}
catch {
    try {
        clean_up();
    }
    catch {
        throw CompositeException; # combines user_code() and clean_up() exceptions
    }
    throw; # rethrow user_code() exception
}
clean_up();

我觉得应该有办法避免在上面的例子中重复clean_up()行,但我想不出来。

简而言之,在不知道你认为两个部分都会发生什么的情况下,你的问题就无法解决了。

答案 1 :(得分:0)

更新:以下方法似乎没有像Eric所说的那样有效。

我将离开这个答案,万一有人可以把它变成工作状态。

问题是:

我预计,一旦本地超出范围将弹出旧的全局值返回全局绑定变量将涉及对FETCH / STORE的调用,但不知何故它只是静默地发生而没有达到绑定机制(问题与异常处理)。


Schwern - 我不是100%确定你可以使用领带技术(从Perlmonks post by Abigail偷来的)用于你的用例 - 这是我试图做我认为你想做的事情

use Test::More tests => 6;

my $guard_triggered = 0;
sub user_cleanup { $guard_triggered++; die "En guarde!" }; # Line 4;
sub TIESCALAR {bless \(my $dummy) => shift}
sub FETCH     { user_cleanup(); }
sub STORE     {1;}
our $guard;
tie $guard => __PACKAGE__; # I don't think the actual value matters

sub x {
    my $x = 1; # Setup
    local $guard = "in x";
    my $y = 2; #user_code;
}

sub x2 {
    my $x = 1; # Setup
    local $guard = "in x2";
    die "you bastard"; #user_code;
}

ok !eval {
    x();
}, "the guard died";
is $@, "En guarde! at $0 line 4.\n",    "with the right error message";
is $guard_triggered, 1,                 "the guard worked";

ok !eval {
    x2();
}, "the guard died";
is $@, "En guarde! at $0 line 4.\n",    "with the right error message";
is $guard_triggered, 2,                 "the guard worked";

输出:

1..6
ok 1 - the guard died
ok 2 - with the right error message
ok 3 - the guard worked
ok 4 - the guard died
ok 5 - with the right error message
ok 6 - the guard worked