开发者

Is it possible for a Perl subroutine to force its caller to return?

If I have Perl module like

 package X;

and an object like

 my $x = X->new ();

Inside X.pm, I write an error handler for $x called handle_error, and I call it

 sub check_size
 {
     if ($x->{size} > 1000) {
   开发者_Python百科      $x->handle_error ();
         return;
     }
 }

Is there any way to make handle_error force the return from its caller routine? In other words, in this example, can I make handle_error do return in check_size without actually writing return there?


The only reasonable way to backtrack multiple levels up the call stack is by throwing an exception/dying.

That said, you really should reconsider what you want to do. A programmer (including yourself, six months from now) will expect that, when a function call completes, the statement after it will execute (unless an exception is thrown). Violating that expectation will cause bugs which are caused in handle_error, but appear to be with the code which called handle_error, making them extremely difficult to debug. This is Not A Good Thing.

You're also making the assumption that there is absolutely no situation in which continuing after an error is handled will be appropriate. Hardcoding an assumption like that is practically a sure guarantee that, as soon as you've had time to forget it, you'll come across a case where you need to continue after calling handle_error (and then waste huge amounts of time trying to figure out why the code after handle_error doesn't get run).

And then there's the assumption that you'll always want to skip back exactly two levels in the call stack. That's another assumption that will fail as soon as it's hardcoded. Not only will there be cases where the calling code should continue, there will also be cases where you need to go three levels up the call stack.

So just have handle_error exit by calling die instead of return and trap the exception at the appropriate level where execution should continue. You don't know every place where the sub will ever be called, so you can't predict how many levels back it will need to go.

In the code at hand, if the extra line to just say return is bothering you, you can use return $x->handle_error; You can even get rid of the enclosing scope and make it return $x->handle_error if $x->{size} > 1000; There - three lines removed rather than just one, plus a pair of braces and two pairs of parentheses as a free bonus.

Finally, I would also suggest changing the name of handle_error to better reflect what it actually does. (report_error, maybe?) "Handling an error" usually means cleaning things up to resolve the error so that execution to continue. If you want your handle_error to prevent the code which called it from continuing, then it seems very unlikely that it's cleaning things up to make continuation possible and, once again, it will cause nasty, hard-to-debug surprises for future programmers using this code.


You can use goto &NAME, your return from the error handler will return to the point where check_size was called.

sub check_size { my $x = shift; # you don't say where the $x comes fro in X.pm.
# I assume it is the invocant.

if( $x->{size} > 1000 ) {
    my $sub = $x->can('handle_error');
    goto $sub;
}

}

This works because goto &NAME transfers control to the called function without creating a new stack frame.

I use can to get a reference to the handle_error for $x, so that the method will work properly with subclasses that override handle_error.

This design seems like a bad idea to me, though.

Perhaps this is a good place to use exceptions:

use Try::Tiny;
my $x = X->new();

try   {  $x->check_size }
catch {  $x->handle_error };


The answer to your question is extremely difficult, and I'm not even going to field it because its a poor solution to your real problem. What's the real problem? The problem is that you want an error inside a deeply nested subroutine call to bubble up the stack. This is what exceptions are for.

Here's your code rewritten to throw an exception using croak.

package X;

sub new {
    my $class = shift;
    my %args = @_;
    my $obj = bless \%args, $class;

    $obj->check_size;

    return $obj;
}

my $Max_Size = 1000;
sub check_size
{
    my $self = shift;

    if ($self->{size} > $Max_Size) {
        croak "size $self->{size} is too large, a maximum of $Max_Size is allowed";
   }
}

Then when the user creates an invalid object...

my $obj = X->new( size => 1234 );

check_size dies and throws its exception up the stack. If the user does nothing to stop it, they get an error message "size 1234 is too large, a maximum of 1000 is allowed at somefile line 234". croak makes sure the error message happens at the point where new is called, where the user made the error, not somewhere deep inside X.pm.

Or they can write the new() in an eval BLOCK to trap the error.

my $obj = eval { X->new( size => 1234 ) } or do {
    ...something if the object isn't created...
};

If you want to do something more when an error occurs, you can wrap croak in a method call.

sub error {
    my $self = shift;
    my $error = shift;

    # Leaving log_error unwritten.
    $self->log_error($error);
    croak $error;
}

my $Max_Size = 1000;
sub check_size
{
    my $self = shift;

    if ($self->{size} > $Max_Size) {
        $self->error("size $self->{size} is too large, a maximum of $Max_Size is allowed");
   }
}

The exception from croak will bubble up the stack through error, check_size and new.

As daotoad points out, Try::Tiny is a better exception handler than a straight eval BLOCK.

See Should a Perl constructor return an undef or a "invalid" object? for more reasons why exceptions are a good idea.


You could use Continuation::Escape. This would basically allow you to pass the "point-of-return" on to the error handler.


Workaround that works but is not nice to see.

Use the select statement.

If you have a function my_func() which you call and then based on the return value you want to do some error processing and then return from the caller you could do:

($result = my_func()) == 0 ? return handle_error($result) : 0;

This is a (ugly) one-liner that substitutes:

$result = my_func();
if ($result == 0) {
    handle_error($result);

    return;
}

Convenient if you have many function to call and check return value.

0

上一篇:

下一篇:

精彩评论

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

最新问答

问答排行榜