开发者

Need an end of lexical scope action which can die normally

开发者 https://www.devze.com 2023-02-02 10:45 出处:网络
I need the ability to add actions to the end of a lexical block where the action might die.And I need the exception to be thrown normally and be able to be caught normally.

I need the ability to add actions to the end of a lexical block where the action might die. And I need the exception to be thrown normally and be able to be caught normally.

Unfortunately, Perl special cases exceptions during DESTROY both by adding "(in cleanup)" to the message and making them untrappable. For example:

{
    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";

I want that to pass. Currently the exception is totally swallowed by the eval.

This is for Test::Builder2, so I cannot use anything but pure Perl.

The underlying issue is I have code like this:

{
    $self->setup;

    $user_code->();

    $self->cleanup;
}

That cleanup must happen even if the $user_code dies, else $self gets into a weird state. So I did this:

开发者_如何学JAVA
{
    $self->setup;

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

    $user_code->();
}

The complexity comes because the cleanup runs arbitrary user code and it is a use case where that code will die. I expect that exception to be trappable and unaltered by the guard.

I'm avoiding wrapping everything in eval blocks because of the way that alters the stack.


Is this semantically sound? From what I understand, you have this (in pseudocode):

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

There are two possibilities:

  • user_code() and clean_up() will never throw in the same run, in which case you can just write it as sequential code without any funny guard business and it will work.
  • user_code() and clean_up() may, at some point, both throw in the same run.

If both functions may throw, then you have two active exceptions. I don't know any language which can handle multiple active currently thrown exceptions, and I'm sure there's a good reason for this. Perl adds (in cleanup) and makes the exception untrappable; C++ calls terminate(), Java drops the original exception silently, etc etc.

If you have just come out of an eval in which both user_code() and cleanup() threw exceptions, what do you expect to find in $@?

Usually this indicates you need to handle the cleanup exception locally, perhaps by ignoring the cleanup exception:

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

or you need to choose an exception to ignore when both throw (which is what DVK's solution does; it ignores the user_code() exception):

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;

or somehow combine the two exceptions into one exception object:

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

I feel there should be a way to avoid repeating the clean_up() line in the above example, but I can't think of it.

In short, without knowing what you think should happen when both parts throw, your problem cannot be answered.


UPDATE: The approach below doesn't seem to work as written as Eric noted!.

I'm leaving this answer up in case someone can wrangle it into working shape.

The problem is:

I expected that popping old global value back onto the global tied variable once the local one goes out of scope will involve a call to FETCH/STORE, but somehow it just happens silently without hitting the tied mechanism (the issue is irrelevant to exception handling).


Schwern - I'm not 100% sure you can use the tie technique (stolen from Perlmonks post by Abigail) for your use case - here's my attempt to do what I think you were trying to do

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";

OUTPUT:

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
0

精彩评论

暂无评论...
验证码 换一张
取 消