¿Es el uso de la cláusula finalmente para hacer el trabajo después de devolver un mal estilo / peligroso?

30

Como parte de la escritura de un iterador, me encontré escribiendo el siguiente código (eliminación de errores de manejo)

public T next() {
  try {
    return next;
  } finally {
    next = fetcher.fetchNext(next);
  }
}

resulta un poco más fácil de leer que

public T next() {
  T tmp = next;
  next = fetcher.fetchNext(next);
  return tmp;
}

Sé que es un ejemplo simple, donde la diferencia en la legibilidad puede no ser tan abrumadora, pero estoy interesado en la opinión general sobre si es malo usar try-finally en casos como este donde no hay excepciones involucradas, o si realmente se prefiere cuando simplifica el código.

Si es malo: ¿por qué? Estilo, rendimiento, trampas, ...?

Conclusión Gracias a todas sus respuestas! Supongo que la conclusión (al menos para mí) es que el primer ejemplo podría haber sido más legible si fuera un patrón común, pero que no lo es. Por lo tanto, la confusión introducida al usar una construcción fuera de su propósito, junto con el flujo de excepciones posiblemente ofuscado, superará cualquier simplificación.

Halle Knast
fuente
10
Yo personalmente podría entender el segundo más que el primero, pero rara vez uso finallybloques.
Christian Mann
2
return current = fetcher.fetchNext (actual); // ¿Qué tal esto, también conocido como realmente necesitas una captación previa?
scarfridge
1
@scarfridge El ejemplo es acerca de la implementación Iterator, donde de hecho necesita algún tipo de captación previa para hasNext()poder trabajar. Inténtalo tú mismo.
maaartinus
1
@maaartinus Soy consciente de eso. A veces hasNext () simplemente devuelve verdadero, por ejemplo, secuencia de granizo y, a veces, recuperar el siguiente elemento es prohibitivamente costoso. En este último caso, debe recurrir a buscar el elemento solo bajo demanda, similar a la inicialización diferida.
scarfridge
1
@scarfridge El problema con el uso común de Iteratores que necesita recuperar el valor hasNext()(porque recuperarlo es a menudo la única forma de averiguar si existe) y devolverlo next()como lo hizo el OP.
maaartinus

Respuestas:

18

Personalmente, pensaría en la idea de la separación de consultas de comando . Cuando llegas al final, next () tiene dos propósitos: el propósito anunciado de recuperar el elemento next, y el efecto secundario oculto de mutar el estado interno de next. Entonces, lo que está haciendo es realizar el propósito anunciado en el cuerpo del método y luego agregar un efecto secundario oculto en una cláusula final, que parece ... incómodo, aunque no 'incorrecto', exactamente.

Lo que realmente se reduce a lo comprensible que es el código, diría yo, y en este caso la respuesta es "meh". Está "probando" una simple declaración de retorno y luego está ejecutando un efecto secundario oculto en un bloque de código que se supone que es para la recuperación de errores a prueba de fallas. Lo que estás haciendo es 'inteligente', y el código 'inteligente' a menudo induce a los mantenedores a murmurar: "qué ... oh, creo que lo entiendo". Mejor para las personas que leen su código para murmurar "sí, uh-huh, tiene sentido, sí ..."

Entonces, ¿qué pasa si separas la mutación de estado de las llamadas de acceso? Me imagino que el problema de legibilidad que le preocupa se vuelve discutible, pero no sé cómo eso afecta su abstracción más amplia. Algo a tener en cuenta, de todos modos.

Erik Dietrich
fuente
1
+1 para el comentario "inteligente". Eso captura con mucha precisión este ejemplo.
2
La acción 'regresar y avanzar' de next () es forzada sobre el OP por la difícil interfaz del iterador de Java.
Kevin Cline
37

Desde el punto de vista del estilo, creo que estas tres líneas:

T current = next;
next = fetcher.getNext();
return current;

... son más obvios y más cortos que un bloque try / finally. Dado que no espera que se produzcan excepciones, el uso de un trybloqueo solo confundirá a las personas.

StriplingWarrior
fuente
2
Un bloque try / catch / finally también podría agregar una sobrecarga adicional, no estoy seguro de si javac o el compilador JIT los eliminarían incluso si no está lanzando una Excepción.
Martijn Verburg
2
@MartijnVerburg, sí, javac todavía agrega una entrada a la tabla de excepciones y duplica el código finalmente, incluso si el bloque try está vacío.
Daniel Lubarov
@Daniel - gracias, fui demasiado flojo para ejecutar javap :-)
Martijn Verburg el
@Martijn Verburg: AFAIK, un bloque try-catch-finallt es esencialmente gratuito siempre que no se produzca una excepción real. Aquí, javac no intenta y no necesita optimizar nada, ya que JIT se encargará de ello.
maaartinus
@maaartinus - gracias, eso tiene sentido - necesita leer el código fuente de OpenJDK nuevamente :-)
Martijn Verburg
6

Eso dependería del propósito del código dentro del bloque finalmente. El ejemplo canónico es cerrar un flujo después de leerlo / escribirlo, algún tipo de "limpieza" que siempre debe hacerse. Restaurar un objeto (en este caso un iterador) a un estado válido en mi humilde opinión también cuenta como limpieza, por lo que no veo ningún problema aquí. Si OTOH estaba usando returntan pronto como se encontró su valor de retorno, y agregando una gran cantidad de código no relacionado en el bloque finalmente, entonces oscurecería el propósito y lo haría todo menos comprensible.

No veo ningún problema en usarlo cuando "no hay excepciones involucradas". Es muy común usarlo try...finallysin un catchcódigo cuando el código solo puede arrojarse RuntimeExceptiony no planeas manejarlo. A veces, finalmente es solo una salvaguarda, y usted sabe por la lógica de su programa que nunca se lanzará una excepción (la clásica condición de "esto nunca debería suceder").

Dificultades: cualquier excepción planteada dentro del trybloque hará que el finallybock se ejecute. Eso puede ponerte en un estado inconsistente. Entonces, si su declaración de devolución fuera algo como:

return preProcess(next);

y este código generó una excepción, fetchNextaún se ejecutaría. OTOH si lo codificó como:

T ret = preProcess(next);
next = fetcher.fetchNext(next);
return ret;

entonces no correría. Sé que está asumiendo que el código de prueba nunca puede generar ninguna excepción, pero para casos más complejos que esto, ¿cómo puede estar seguro? Si su iterador fuera un objeto de larga duración, eso continuaría existiendo incluso si ocurriera un error irrecuperable en el hilo actual, entonces sería importante mantenerlo en un estado válido en todo momento. De lo contrario, realmente no importa mucho ...

Rendimiento: sería interesante descompilar dicho código para ver cómo funciona bajo el capó, pero no conozco lo suficiente de la JVM como para adivinarlo ... Las excepciones suelen ser "excepcionales", por lo que el código que maneja no es necesario que estén optimizados para la velocidad (de ahí el consejo de nunca usar excepciones en el flujo de control normal de sus programas), pero no lo sé finally.

mgibsonbr
fuente
¿No estás proporcionando una buena razón para evitar usar finallyde esta manera, entonces? Quiero decir, como dices, el código termina teniendo consecuencias no deseadas; peor aún, probablemente ni siquiera pienses en excepciones.
Andres F.
2
La velocidad de flujo de control normal no se ve afectada significativamente por las construcciones de manejo de excepciones. La penalización de rendimiento solo se paga cuando se lanzan y capturan excepciones, no cuando se ingresa a un bloque de prueba o se ingresa a un bloque finalmente en operación normal.
yfeldblum
1
@AndresF. Es cierto, pero eso también es válido para cualquier código de limpieza. Por ejemplo, supongamos que un nullcódigo con errores establece una referencia a una secuencia abierta ; intentar leerlo generará una excepción, intentar cerrarlo en el finallybloque generará otra excepción. Además, esta es una situación en la que el estado del cálculo no importa, desea cerrar la transmisión independientemente de si se produjo una excepción. Puede haber otras situaciones como esa también. Por esta razón, lo estoy tratando como una trampa para los usos válidos, en lugar de una recomendación para nunca usarlo.
mgibsonbr
También existe el problema de que cuando tanto el intento como el bloqueo final arrojan una excepción, nadie puede ver la excepción en el intento, pero probablemente sea la causa del problema.
Hans-Peter Störr
3

La declaración del título: "... finalmente cláusula para hacer el trabajo después del regreso ..." es falsa. El último bloque ocurre antes de que regrese la función Ese es el punto final de hecho.

Lo que está abusando aquí es el orden de evaluación, donde el valor de next se almacena para regresar antes de mutarlo. No es una práctica común y, en mi opinión, es incorrecta, ya que hace que el código no sea secuencial y, por lo tanto, sea mucho más difícil de seguir.

El uso previsto de los bloques finalmente es para el manejo de excepciones, donde deben ocurrir algunas consecuencias independientemente de las excepciones lanzadas, por ejemplo, limpiar algunos recursos, cerrar la conexión de base de datos, cerrar un archivo / socket, etc.

Nitsan Wakart
fuente
+1, agregaría que incluso si la especificación del idioma garantiza que el valor se almacena antes de que se devuelva, es potencialmente contrario a las expectativas del lector y, por lo tanto, debe evitarse cuando sea razonablemente posible. La depuración es el doble de difícil que la codificación ...
jmoreno
0

Sería muy cuidadoso, porque (en general, no en su ejemplo) parte en try puede arrojar una excepción, y si se descarta, no se devolverá nada y si realmente tiene la intención de ejecutar lo que está finalmente después del retorno, lo hará ejecutar cuando no lo quisieras.

Y otra lata de gusanos es que, en general, alguna acción útil finalmente puede generar excepciones, por lo que puede terminar con un método realmente desordenado.

Debido a esto, alguien que lo lea debe pensar en estos problemas, por lo que es más difícil de entender.

user470365
fuente