A menudo hablo con programadores que dicen " No pongas múltiples declaraciones de retorno en el mismo método " . Cuando les pido que me digan las razones, todo lo que obtengo es " El estándar de codificación lo dice " o " Es confuso " . Cuando me muestran soluciones con una sola declaración de devolución, el código me parece más feo. Por ejemplo:
if (condition)
return 42;
else
return 97;
" Esto es feo, ¡tienes que usar una variable local! "
int result;
if (condition)
result = 42;
else
result = 97;
return result;
¿Cómo hace que esta extensión de código del 50% haga que el programa sea más fácil de entender? Personalmente, me resulta más difícil, porque el espacio de estado acaba de aumentar por otra variable que fácilmente podría haberse evitado.
Por supuesto, normalmente solo escribiría:
return (condition) ? 42 : 97;
Pero muchos programadores evitan el operador condicional y prefieren la forma larga.
¿De dónde vino esta noción de "solo un retorno"? ¿Hay alguna razón histórica por la que surgió esta convención?
fuente
Respuestas:
"Entrada única, salida única" se escribió cuando la mayoría de la programación se realizó en lenguaje ensamblador, FORTRAN o COBOL. Se ha malinterpretado ampliamente, porque los idiomas modernos no son compatibles con las prácticas contra las que Dijkstra estaba advirtiendo.
"Entrada única" significa "no crear puntos de entrada alternativos para funciones". En lenguaje ensamblador, por supuesto, es posible ingresar una función en cualquier instrucción. FORTRAN admite múltiples entradas a funciones con la
ENTRY
declaración:"Salida única" significa que una función solo debe regresar a un lugar: la declaración que sigue inmediatamente a la llamada. No pasó no quiere decir que una función sólo debe volver a un lugar. Cuando se escribió la programación estructurada , era una práctica común que una función indicara un error al regresar a una ubicación alternativa. FORTRAN apoyó esto a través de "retorno alternativo":
Ambas técnicas fueron altamente propensas a errores. El uso de entradas alternativas a menudo dejaba algunas variables sin inicializar. El uso de retornos alternativos tenía todos los problemas de una declaración GOTO, con la complicación adicional de que la condición de la rama no era adyacente a la rama, sino en algún lugar de la subrutina.
fuente
const
desde antes de que nacieran muchos de los usuarios aquí, por lo que ya no hay necesidad de constantes de capital Pero incluso en C. Java conserva todos esos malos hábitos viejos C .setjmp/longjmp
?)Esta noción de entrada única, salida única (SESE) proviene de lenguajes con gestión explícita de recursos , como C y ensamblado. En C, un código como este filtrará recursos:
En tales idiomas, básicamente tiene tres opciones:
Replicar el código de limpieza.
Ugh La redundancia siempre es mala.
Use a
goto
para saltar al código de limpieza.Esto requiere que el código de limpieza sea lo último en la función. (Y esta es la razón por la cual algunos argumentan que
goto
tiene su lugar. Y de hecho lo ha hecho, en C.)Introduzca una variable local y manipule el flujo de control a través de eso.
La desventaja es que el flujo de control manipulado a través de la sintaxis (piense
break
,return
,if
,while
) es mucho más fácil de seguir que el flujo de control manipulado por el estado de las variables (ya que estas variables no tienen ningún estado cuando nos fijamos en el algoritmo).En el ensamblaje es aún más extraño, porque puede saltar a cualquier dirección en una función cuando llama a esa función, lo que significa que tiene un número casi ilimitado de puntos de entrada a cualquier función. (A veces esto es útil. Tales thunks son una técnica común para que los compiladores implementen el
this
ajuste de puntero necesario para llamar avirtual
funciones en escenarios de herencia múltiple en C ++).Cuando tiene que administrar los recursos manualmente, explotar las opciones de ingresar o salir de una función en cualquier lugar conduce a un código más complejo y, por lo tanto, a errores. Por lo tanto, apareció una escuela de pensamiento que propagó SESE, para obtener un código más limpio y menos errores.
Sin embargo, cuando un idioma presenta excepciones, (casi) cualquier función puede salir prematuramente en (casi) cualquier punto, por lo que debe tomar medidas para el retorno prematuro de todos modos. (Creo que
finally
se usa principalmente para eso en Java yusing
(cuando se implementaIDisposable
, de lofinally
contrario) en C #; C ++ en su lugar emplea RAII .) Una vez que haya hecho esto, no puede dejar de limpiar después de usted debido a unareturn
declaración temprana , entonces lo que probablemente sea El argumento más fuerte a favor de SESE ha desaparecido.Eso deja legibilidad. Por supuesto, una función de 200 LoC con media docena de
return
declaraciones esparcidas aleatoriamente sobre ella no es un buen estilo de programación y no genera código legible. Pero tal función tampoco sería fácil de entender sin esos retornos prematuros.En los idiomas donde los recursos no se administran o no se deben administrar manualmente, hay poco o ningún valor en adherirse a la antigua convención SESE. OTOH, como he argumentado anteriormente, SESE a menudo hace que el código sea más complejo . Es un dinosaurio que (a excepción de C) no encaja bien en la mayoría de los lenguajes actuales. En lugar de ayudar a la comprensión del código, lo dificulta.
¿Por qué los programadores de Java se adhieren a esto? No lo sé, pero desde mi punto de vista (externo), Java tomó muchas convenciones de C (donde tienen sentido) y las aplicó a su mundo OO (donde son inútiles o directamente malas), donde ahora se adhiere a ellos, sin importar los costos. (Al igual que la convención para definir todas sus variables al comienzo del alcance).
Los programadores se adhieren a todo tipo de notaciones extrañas por razones irracionales. (Las declaraciones estructurales profundamente anidadas - "puntas de flecha" - fueron vistas, en lenguajes como Pascal, una vez como un código hermoso.) Aplicar un razonamiento lógico puro a esto parece no convencer a la mayoría de ellos de desviarse de sus formas establecidas. La mejor manera de cambiar tales hábitos es probablemente enseñarles desde el principio a hacer lo mejor, no lo convencional. Usted, como profesor de programación, lo tiene en la mano.
:)
fuente
finally
cláusulas donde se ejecuta independientemente de las primerasreturn
o excepciones.finally
mayor parte del tiempo.malloc()
yfree()
en un comentario como ejemplo), estaba hablando de recursos en general. Tampoco estaba implicando que GC resolvería estos problemas. (Mencioné C ++, que no tiene GC fuera de la caja). Por lo que entiendo, en Javafinally
se usa para resolver este problema.Por un lado, las declaraciones de retorno únicas facilitan el registro, así como las formas de depuración que dependen del registro. Recuerdo muchas veces que tuve que reducir la función a un solo retorno solo para imprimir el valor de retorno en un solo punto.
Por otro lado, podría refactorizar esto en
function()
esas llamadas_function()
y registrar el resultado.fuente
_function()
, conreturn
s en los lugares apropiados, y un contenedor llamadofunction()
que maneje el registro extraño, que tener un solofunction()
con lógica retorcida para que todos los retornos encajen en una sola salida -point solo para poder insertar una declaración adicional antes de ese punto."Entrada única, salida única" se originó con la revolución de la programación estructurada de principios de la década de 1970, que fue iniciada por la carta de Edsger W. Dijkstra al editor " Declaración GOTO considerada dañina ". Los conceptos detrás de la programación estructurada se expusieron en detalle en el libro clásico "Programación estructurada" de Ole Johan-Dahl, Edsger W. Dijkstra y Charles Anthony Richard Hoare.
La "Declaración GOTO considerada perjudicial" es una lectura obligatoria, incluso hoy. La "Programación estructurada" está anticuada, pero sigue siendo muy, muy gratificante, y debe estar en la parte superior de la lista "Debe leer" de cualquier desarrollador, muy por encima de cualquier cosa, por ejemplo, de Steve McConnell. (La sección de Dahl presenta los conceptos básicos de las clases en Simula 67, que son la base técnica para las clases en C ++ y toda la programación orientada a objetos).
fuente
goto
que literalmente podía ir a cualquier parte , como a un punto aleatorio en otra función, sin pasar por ninguna noción de procedimientos, funciones, una pila de llamadas, etc. Ningún lenguaje sensato lo permite en estos días con una rectagoto
. C'ssetjmp
/longjmp
es el único caso semi-excepcional que conozco, e incluso eso requiere la cooperación de ambos extremos. (Sin embargo, parece irónico que utilicé la palabra "excepcional", considerando que las excepciones hacen casi lo mismo ...) Básicamente, el artículo desalienta una práctica que está muerta desde hace mucho tiempo.Siempre es fácil vincular Fowler.
Uno de los principales ejemplos que van en contra de SESE son las cláusulas de guardia:
Reemplazar condicional anidado con cláusulas de guardia
fuente
_isSeparated
y_isRetired
puede ser tanto verdadera (y por qué no sería eso posible?) Que devuelva el importe incorrecto.Escribí una publicación de blog sobre este tema hace un tiempo.
La conclusión es que esta regla proviene de la época de los idiomas que no tienen recolección de basura o manejo de excepciones. No existe un estudio formal que demuestre que esta regla conduzca a un mejor código en los idiomas modernos. Siéntase libre de ignorarlo siempre que esto conduzca a un código más corto o más legible. Los chicos de Java que insisten en esto son ciegos e incuestionables siguiendo una regla obsoleta y sin sentido.
Esta pregunta también se ha hecho en Stackoverflow
fuente
Una devolución facilita la refactorización. Intente realizar el "método de extracción" en el cuerpo interno de un bucle for que contenga un retorno, una interrupción o una continuación. Esto fallará ya que ha roto su flujo de control.
El punto es: supongo que nadie finge escribir código perfecto. Por lo tanto, el código está bajo refactorización para ser "mejorado" y extendido. Por lo tanto, mi objetivo sería mantener mi código tan amigable como sea posible.
A menudo me enfrento al problema de que tengo que reformular completamente las funciones si contienen interruptores de flujo de control y si quiero agregar solo poca funcionalidad. Esto es muy propenso a errores ya que cambia todo el flujo de control en lugar de introducir nuevas rutas a anidamientos aislados. Si solo tiene un retorno al final o si usa guardias para salir de un ciclo, por supuesto, tendrá más anidamiento y más código. Pero obtienes capacidades de refactorización compatibles con el compilador y el IDE.
fuente
Considere el hecho de que múltiples declaraciones de devolución son equivalentes a tener GOTO a una sola declaración de devolución. Este es el mismo caso con las declaraciones de ruptura. Por lo tanto, algunos, como yo, los consideran GOTO para todos los efectos.
Sin embargo, no considero que estos tipos de GOTO sean dañinos y no dudaré en usar un GOTO real en mi código si encuentro una buena razón para ello.
Mi regla general es que los GOTO son solo para control de flujo. Nunca deben usarse para ningún bucle, y nunca debe GOTO 'hacia arriba' o 'hacia atrás'. (que es cómo funcionan los descansos / devoluciones)
Como otros han mencionado, la siguiente es una declaración obligatoria de GOTO considerada perjudicial.
Sin embargo, tenga en cuenta que esto fue escrito en 1970 cuando los GOTO se usaban demasiado. No todos los GOTO son dañinos y no desaconsejaría su uso siempre que no los use en lugar de las construcciones normales, sino que, en el extraño caso, el uso de construcciones normales sería muy inconveniente.
Encuentro que usarlos en casos de error en los que necesita escapar de un área debido a una falla que nunca debería ocurrir en casos normales es útil a veces. Pero también debe considerar poner este código en una función separada para que pueda regresar temprano en lugar de usar un GOTO ... pero a veces eso también es inconveniente.
fuente
Complejidad Ciclomática
He visto a SonarCube usar una declaración de retorno múltiple para determinar la complejidad ciclomática. Entonces, cuanto más son las declaraciones de retorno, mayor es la complejidad ciclomática
Cambio de tipo de devolución
Los retornos múltiples significan que necesitamos cambiar en varios lugares de la función cuando decidimos cambiar nuestro tipo de retorno.
Salida Múltiple
Es más difícil de depurar ya que la lógica debe estudiarse cuidadosamente junto con las declaraciones condicionales para comprender qué causó el valor devuelto.
Solución refactorizada
La solución a las declaraciones de retorno múltiple es reemplazarlas con un polimorfismo que tenga un solo retorno después de resolver el objeto de implementación requerido.
fuente