2010-05-20 12 views
6

Si tengo módulo de Perl como¿Es posible que una subrutina Perl obligue a su interlocutor a regresar?

package X; 

y un objeto como

my $x = X->new(); 

Dentro X.pm, escribo un controlador de errores para $x llamada handle_error, y yo lo llamo

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

¿Hay alguna manera de hacer handle_error forzar el retorno de su rutina de llamada? En otras palabras, en este ejemplo, ¿puedo hacer que handle_error haga return en check_size sin escribir realmente return allí?

+3

Voy a obtener todo OOP naggy y le molestaré sobre la violación de encapsullo al acceder directamente a 'size'. IME, es una buena idea acceder siempre a los atributos a través de llamadas a métodos. Incluso dentro de una clase, hace que la clase sea más heredable y mejora la capacidad de rediseñar los atributos subyacentes sin romper la compatibilidad. Es una prueba de futuro. – daotoad

Respuesta

4

Puede usar goto &NAME, su devolución del controlador de errores volverá al punto donde se llamó al check_size.

sub check_size { my $ x = shift; # usted no dice dónde sale el $ x en X.pm.
# Supongo que es el invocante.

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

}

Esto funciona porque goto &NAME transfiere el control a la función llamada sin crear un nuevo marco de pila.

utilizo can para obtener una referencia a la handle_error para $x, por lo que el método funcionará correctamente con las subclases que anulan handle_error.

Sin embargo, este diseño me parece una mala idea.

Tal vez este es un buen lugar para utilizar excepciones:

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

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

Gracias por la respuesta. Pero quiero forzar esto dentro de 'handle_error' en lugar de hacerlo en la persona que llama. ¿Hay alguna forma de hacer eso? ¿Tal vez no? –

11

La única forma razonable de dar marcha atrás varios niveles de la pila de llamadas es lanzando una excepción/morir.

Dicho esto, usted realmente debe reconsiderar lo que quiere hacer. Un programador (incluyéndose a usted mismo dentro de seis meses) esperará que, cuando se complete una llamada a función, se ejecute la instrucción siguiente (a menos que se produzca una excepción). La violación de esa expectativa causará errores que se causan en handle_error, pero parecen estar con el código que llamó a handle_error, lo que hace que sea extremadamente difícil de depurar. Esto no es bueno.

También está suponiendo que hay absolutamente ninguna situación en la que continuar después de que se haya manejado un error sea apropiado. La codificación de una suposición como esa es prácticamente una garantía segura de que, tan pronto como haya tenido tiempo de olvidarlo, se encontrará con un caso en el que debe continuar después de llamar al handle_error (y luego perderá una gran cantidad de tiempo tratando de figurar por qué el código después de handle_error no se ejecuta).

Y luego está el supuesto de que se le siempre quiere saltar hacia atrás exactamente dos niveles en la pila de llamadas. Esa es otra suposición que fallará tan pronto como esté codificada. No solo habrá casos en los que el código de llamada deba continuar, también habrá casos en los que deba subir tres niveles en la pila de llamadas.

Así que solo tiene handle_error salir llamando al die en lugar de return y atrape la excepción en el nivel apropiado donde la ejecución debe continuar. No conoce todos los lugares en los que se llamará nunca al submarino, por lo que no puede predecir cuántos niveles tendrá que recorrer.

En el código a mano, si la línea extra para simplemente decir return que le molesta, puede utilizar return $x->handle_error; Incluso puede deshacerse del ámbito que lo contiene y que sea return $x->handle_error if $x->{size} > 1000; No - tres líneas eliminadas en lugar de sólo uno, más un par de llaves y dos pares de paréntesis como una bonificación gratuita.

Finalmente, también sugiero cambiar el nombre de handle_error para reflejar mejor lo que realmente hace. (report_error, ¿quizás?) "Manejar un error" generalmente significa limpiar cosas para resolver el error para que la ejecución continúe. Si quiere que su handle_error evite que el código que lo llamó continúe, entonces parece muy improbable que esté limpiando cosas para hacer posible la continuación y, una vez más, causará sorpresas desagradables y difíciles de depurar para los futuros programadores que usen este código.

+0

Me gustaría saber si hay alguna manera de hacer esto, incluso de una manera irracional. –

+5

@Kinopiko, es posible que pueda hacerlo con un código XS loco. Pero, IMO, este diseño suena como caminar descalzo sobre vidrios rotos para llegar a un arma para poder dispararse en el pie. Pero, ¿qué diablos? Son tus pies. – daotoad

2

La respuesta a su pregunta es extremadamente difícil, y ni siquiera voy a presentarla porque es una solución pobre a su problema real. ¿Cuál es el verdadero problema? El problema es que quiere un error dentro de una llamada de subrutina profundamente anidada para hacer saltar la pila. Esto es para lo que son las excepciones.

Aquí está su código reescrito para lanzar una excepción usando 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"; 
    } 
} 

Luego, cuando el usuario crea un objeto no válido ...

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

check_size troqueles y lanza su excepción en la pila. Si el usuario no hace nada para detenerlo, recibe un mensaje de error "size 1234 is too large, a maximum of 1000 is allowed at somefile line 234". croak se asegura de que el mensaje de error ocurra en el punto donde new es llamado, donde el usuario cometió el error, no en algún lugar profundo dentro de X.pm.

O pueden escribir el nuevo() en un eval BLOCK para atrapar el error.

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

Si quieres hacer algo más cuando se produce un error, se puede envolver croak en una llamada al método.

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

La excepción de croak burbuja voluntad hasta la pila a través error, check_size y new.

Como señala el daotoad, Try :: Tiny es un mejor manejador de excepciones que un eval BLOCK recto.

Consulte Should a Perl constructor return an undef or a "invalid" object? por más motivos por los que las excepciones son una buena idea.

0

Solución que funciona pero que no es agradable de ver.

Use la instrucción select.

Si usted tiene un mi_func función() que se llama y luego sobre la base del valor de retorno que quiere hacer algo de procesamiento de error y luego regresar a la persona que llama que podría hacer:

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

Esta es una (feo) one-liner que sustituye:

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

    return; 
} 

Conveniente si tiene muchas funciones para llamar y comprobar el valor de retorno.

Cuestiones relacionadas