Может ли подпрограмма Perl заставить своего вызывающего вернуться? - PullRequest
6 голосов
/ 20 мая 2010

Если у меня Perl-модуль типа

 package X;

и объект типа

 my $x = X->new ();

Внутри X.pm , я пишу обработчик ошибок для $x с именем handle_error и называю его

 sub check_size
 {
     if ($x->{size} > 1000) {
         $x->handle_error ();
         return;
     }
 }

Есть ли способ заставить handle_error форсировать возврат из процедуры вызова? Другими словами, в этом примере я могу сделать handle_error do return в check_size, не записывая return там?

Ответы [ 5 ]

11 голосов
/ 20 мая 2010

Единственный разумный способ откатить несколько уровней вверх по стеку вызовов - это сгенерировать исключение / смерть.

Тем не менее, вы действительно должны пересмотреть то, что вы хотите сделать. Программист (включая вас самих, через шесть месяцев) будет ожидать, что, когда вызов функции завершится, оператор после него выполнится (если не будет сгенерировано исключение). Нарушение этого ожидания приведет к ошибкам, вызванным в handle_error, но, по-видимому, с кодом, который называется handle_error, что делает их чрезвычайно трудными для отладки. Это нехорошо.

Вы также исходите из предположения, что абсолютно не существует ситуации , в которой будет целесообразно продолжить после обработки ошибки. Жесткое кодирование такого предположения практически является гарантией того, что, как только вы успеете забыть это, вы столкнетесь с ситуацией, когда вам нужно продолжить после вызова handle_error (а затем потратить огромное количество времени на попытки выяснить, почему код после handle_error не запускается).

И затем есть предположение, что вы всегда захотите пропустить точно два уровня в стеке вызовов. Это еще одно предположение, которое потерпит неудачу, как только оно будет жестко закодировано. Не только будут случаи, когда вызывающий код должен продолжаться, но также будут случаи, когда вам нужно подняться на три уровня вверх по стеку вызовов.

Так что просто выйдите из handle_error, вызвав die вместо return, и перехватите исключение на соответствующем уровне, где выполнение должно продолжаться. Вы не знаете каждого места, где когда-либо будет вызываться саб, поэтому вы не можете предсказать, сколько уровней нужно будет пройти назад.

В указанном коде, если вам мешает дополнительная строка, скажем return, вы можете использовать return $x->handle_error; Вы даже можете избавиться от ограждающей области и сделать ее return $x->handle_error if $x->{size} > 1000; Там - три строки удалены чем один, плюс пара скобок и две пары скобок в качестве бесплатного бонуса.

Наконец, я бы также предложил изменить имя handle_error, чтобы лучше отразить то, что он на самом деле делает. (report_error, может быть?) «Обработка ошибки» обычно означает очистку, чтобы устранить ошибку и продолжить выполнение. Если вы хотите, чтобы ваш handle_error препятствовал продолжению кода, вызвавшего его, то очень маловероятно, что он очищает вещи, чтобы сделать возможным продолжение, и, опять же, это вызовет неприятные, трудно отлаживаемые сюрпризы для будущих программистов. используя этот код.

4 голосов
/ 20 мая 2010

Вы можете использовать goto & NAME , ваш возврат из обработчика ошибок вернется к точке, где был вызван check_size.

sub check_size { мой $ x = сдвиг; # Вы не говорите, откуда в X.pm появляется $ x.
# Я предполагаю, что это инвокант.

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

}

Это работает, потому что goto &NAME передает управление вызываемой функции без создания нового фрейма стека.

Я использую can, чтобы получить ссылку на handle_error для $x, чтобы метод правильно работал с подклассами, которые переопределяют handle_error.

Этот дизайн мне кажется плохой идеей.

Возможно, это хорошее место для использования исключений:

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

try   {  $x->check_size }
catch {  $x->handle_error };
2 голосов
/ 21 мая 2010

Ответ на ваш вопрос чрезвычайно труден, и я даже не собираюсь его обсуждать, потому что это плохое решение вашей реальной проблемы. В чем реальная проблема? Проблема в том, что вы хотите, чтобы внутри глубоко вложенного вызова подпрограммы произошла ошибка, которая вызвала пузырь в стеке. Для этого есть исключения.

Вот ваш код, переписанный для исключения, используя 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";
   }
}

Тогда, когда пользователь создает недопустимый объект ...

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

check_size умирает и выбрасывает свое исключение в стек. Если пользователь ничего не делает, чтобы остановить его, они получают сообщение об ошибке "size 1234 is too large, a maximum of 1000 is allowed at somefile line 234". croak убедитесь, что сообщение об ошибке происходит в точке, где new вызываемый, где пользователь допустил ошибку, а не где-то глубоко внутри X.pm.

Или они могут написать new () в eval BLOCK, чтобы перехватить ошибку.

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

Если вы хотите сделать что-то еще при возникновении ошибки, вы можете перенести croak в вызове метода.

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

Исключение из croak будет подниматься в стек через error, check_size и new.

Как отмечает daotoad, Try :: Tiny - лучший обработчик исключений, чем прямой eval BLOCK.

См. Должен ли конструктор Perl возвращать undef или "недопустимый" объект? по другим причинам, почему исключения являются хорошей идеей.

1 голос
/ 20 мая 2010

Вы можете использовать Продолжение :: Побег . Это в основном позволит вам передать «точку возврата» обработчику ошибок.

0 голосов
/ 17 июля 2013

Обходной путь, который работает, но не очень приятно видеть.

Используйте оператор выбора.

Если у вас есть функция my_func (), которую вы вызываете, а затем на основе возвращаемого значения вы хотите выполнить некоторую обработку ошибок, а затем вернуться к вызывающей стороне, которую вы можете сделать:

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

Это (безобразный) однострочник, который заменяет:

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

    return;
}

Удобно, если у вас много функций для вызова и проверки возвращаемого значения.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...