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.
精彩评论