¿De dónde viene la noción de "solo un retorno"?

1055

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?

flujo libre
fuente
2
Esto está algo relacionado con la refactorización de la cláusula de guardia. stackoverflow.com/a/8493256/679340 Guard Clause agregará retornos al comienzo de sus métodos. Y hace que el código sea mucho más limpio en mi opinión.
Piotr Perak
3
Proviene de la noción de programación estructurada. Algunos pueden argumentar que tener solo una devolución le permite modificar fácilmente el código para hacer algo justo antes de regresar o depurar fácilmente.
martinkunev
3
Creo que el ejemplo es un caso bastante simple en el que no tendría una opinión sólida de una manera u otra. ¡El ideal de entrada única-salida única es más para alejarnos de situaciones locas como 15 declaraciones de devolución y dos ramas más que no regresan en absoluto!
mendota
2
Ese es uno de los peores artículos que he leído. Parece que el autor pasa más tiempo fantaseando acerca de la pureza de su POO que imaginando cómo lograr algo. Los árboles de expresión y evaluación tienen valor, pero no cuando solo puedes escribir una función normal.
DeadMG
3
Debe eliminar la condición por completo. La respuesta es 42.
cambunctious

Respuestas:

1120

"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 ENTRYdeclaración:

      SUBROUTINE S(X, Y)
      R = SQRT(X*X + Y*Y)
C ALTERNATE ENTRY USED WHEN R IS ALREADY KNOWN
      ENTRY S2(R)
      ...
      RETURN
      END

C USAGE
      CALL S(3,4)
C ALTERNATE USAGE
      CALL S2(5)

"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":

C SUBROUTINE WITH ALTERNATE RETURN.  THE '*' IS A PLACE HOLDER FOR THE ERROR RETURN
      SUBROUTINE QSOLVE(A, B, C, X1, X2, *)
      DISCR = B*B - 4*A*C
C NO SOLUTIONS, RETURN TO ERROR HANDLING LOCATION
      IF DISCR .LT. 0 RETURN 1
      SD = SQRT(DISCR)
      DENOM = 2*A
      X1 = (-B + SD) / DENOM
      X2 = (-B - SD) / DENOM
      RETURN
      END

C USE OF ALTERNATE RETURN
      CALL QSOLVE(1, 0, 1, X1, X2, *99)
C SOLUTION FOUND
      ...
C QSOLVE RETURNS HERE IF NO SOLUTIONS
99    PRINT 'NO SOLUTIONS'

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.

kevin cline
fuente
39
Y no olvides el código de espagueti . No era desconocido que las subrutinas salieran usando un GOTO en lugar de un retorno, dejando los parámetros de llamada de función y la dirección de retorno en la pila. La salida única se promocionó como una forma de al menos canalizar todas las rutas de código a una instrucción RETURN.
TMN
2
@ TMN: en los primeros días, la mayoría de las máquinas no tenían una pila de hardware. La recursión generalmente no fue compatible. Los argumentos de subrutina y la dirección de retorno se almacenaron en ubicaciones fijas adyacentes al código de subrutina. El regreso fue solo un goto indirecto.
Kevin Cline
55
@kevin: Sí, pero según usted, esto ya no significa lo que se inventó. (Por cierto, en realidad estoy razonablemente seguro de que Fred preguntó si la preferencia para la interpretación actual de "Single Exit" proviene). Además, C ha tenido constdesde 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 .
sbi
3
Entonces, ¿las excepciones violan esta interpretación de Salida única? (¿O su primo más primitivo setjmp/longjmp?)
Mason Wheeler
2
Aunque el operador le preguntó sobre la interpretación actual del retorno único, esta respuesta es la que tiene más raíces históricas. No tiene sentido usar un solo retorno como regla , a menos que desee que su lenguaje coincida con la genialidad de VB (no .NET). Solo recuerde utilizar también la lógica booleana sin cortocircuito.
Acelerado
912

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:

void f()
{
  resource res = acquire_resource();  // think malloc()
  if( f1(res) )
    return; // leaks res
  f2(res);
  release_resource(res);  // think free()
}

En tales idiomas, básicamente tiene tres opciones:

  • Replicar el código de limpieza.
    Ugh La redundancia siempre es mala.

  • Use a gotopara 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 gototiene 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 thisajuste de puntero necesario para llamar a virtualfunciones 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 finallyse usa principalmente para eso en Java y using(cuando se implementa IDisposable, de lo finallycontrario) 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 una returndeclaració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 returndeclaraciones 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.:)

sbi
fuente
52
Derecha. En Java, el código de limpieza pertenece a finallycláusulas donde se ejecuta independientemente de las primeras returno excepciones.
dan04
15
@ dan04 en Java 7 ni siquiera necesita la finallymayor parte del tiempo.
R. Martinho Fernandes
93
@ Steven: ¡Por supuesto que puedes demostrar eso! De hecho, puede mostrar código complicado y complejo con cualquier función que también se pueda mostrar para hacer que el código sea más simple y fácil de entender. Todo puede ser abusado. El punto es escribir código para que sea más fácil de entender , y cuando eso implica tirar SESE por la ventana, que así sea, y malditos los viejos hábitos que se aplicaban a diferentes idiomas. Pero tampoco dudaría en controlar la ejecución por variables si pensara que hace que el código sea más fácil de leer. Es solo que no recuerdo haber visto ese código en casi dos décadas.
sbi
21
@Karl: De hecho, es una grave deficiencia de los lenguajes GC como Java que te alivian de tener que limpiar un recurso, pero fallan con todos los demás. (C ++ resuelve este problema para todos los recursos que usan RAII .) Pero ni siquiera estaba hablando solo de memoria (solo puse malloc()y free()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 Java finallyse usa para resolver este problema.
sbi
10
@sbi: Más importante para una función (procedimiento, método, etc.) que no tener más de una página es que la función tenga un contrato claramente definido; si no está haciendo algo claro porque se ha cortado para satisfacer una restricción de longitud arbitraria, eso es malo. La programación consiste en jugar fuerzas diferentes, a veces en conflicto entre sí.
Donal Fellows
81

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.

  int function() {
     if (bidi) { print("return 1"); return 1; }
     for (int i = 0; i < n; i++) {
       if (vidi) { print("return 2"); return 2;}
     }
     print("return 3");
     return 3;
  }

Por otro lado, podría refactorizar esto en function()esas llamadas _function()y registrar el resultado.

perreal
fuente
31
También agregaría que facilita la depuración porque solo necesita establecer un punto de interrupción para capturar todas las salidas * de la función. Creo que algunos IDEs le permiten poner un punto de interrupción en la llave estrecha de la función para hacer lo mismo. (* a menos que llame a la salida)
Skizz
3
Por una razón similar, también hace que sea más fácil extender (agregar) la función, ya que su nueva funcionalidad no tiene que ser insertada antes de cada retorno. Supongamos que necesita actualizar un registro con el resultado de la llamada a la función, por ejemplo.
JeffSahol
63
Honestamente, si mantuviera ese código, preferiría tener una definición sensata _function(), con returns en los lugares apropiados, y un contenedor llamado function()que maneje el registro extraño, que tener un solo function()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.
ruakh
11
En algunos depuradores (MSVS) puede poner un punto de interrupción en la última llave de cierre
Abyx
66
impresión! = depuración. Eso no es argumento en absoluto.
Piotr Perak
53

"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).

John R. Strohm
fuente
66
El artículo fue escrito en días anteriores a C cuando los GOTO se usaban mucho. No son el enemigo, pero esta respuesta es definitivamente correcta. Una declaración de retorno que no está al final de una función es efectivamente un goto.
user606723
31
El artículo también fue escrito en los días en gotoque 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 recta goto. C's setjmp/ longjmpes 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.
cHao
55
Del último párrafo de "La declaración Goto se considera dañina": "en [2] Guiseppe Jacopini parece haber demostrado la superfluidad (lógica) de la declaración go to. El ejercicio para traducir un diagrama de flujo arbitrario más o menos mecánicamente en un salto- sin embargo, no se recomienda menos uno . Entonces no se puede esperar que el diagrama de flujo resultante sea más transparente que el original. "
hugomg 9/11
10
¿Qué tiene esto que ver con la pregunta? Sí, el trabajo de Dijkstra finalmente condujo a los idiomas SESE, ¿y qué? Lo mismo hizo el trabajo de Babbage. Y quizás debería volver a leer el documento si cree que dice algo sobre tener múltiples puntos de salida en una función. Porque no lo hace.
jalf
10
@John, parece que estás tratando de responder la pregunta sin responderla realmente. Es una buena lista de lectura, pero no ha citado ni parafraseado nada para justificar su afirmación de que este ensayo y este libro tienen algo que decir sobre la preocupación del autor de la pregunta. De hecho, fuera de los comentarios, no ha dicho nada sustancial sobre la pregunta. Considere ampliar esta respuesta.
Shog9
35

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

Use cláusulas de guardia para todos los casos especiales

double getPayAmount() {
    double result;
    if (_isDead) result = deadAmount();
    else {
        if (_isSeparated) result = separatedAmount();
        else {
            if (_isRetired) result = retiredAmount();
            else result = normalPayAmount();
        };
    }
return result;
};  

                                                                                                         http://www.refactoring.com/catalog/arrow.gif

double getPayAmount() {
    if (_isDead) return deadAmount();
    if (_isSeparated) return separatedAmount();
    if (_isRetired) return retiredAmount();
    return normalPayAmount();
};  

Para obtener más información, consulte la página 250 de Refactorización ...

Pieter B
fuente
11
Otro mal ejemplo: podría solucionarse fácilmente con else-ifs.
Jack
1
Su ejemplo no es justo, ¿qué tal esto: double getPayAmount () {double ret = normalPayAmount (); if (_isDead) ret = deadAmount (); if (_isSeparated) ret = separateAmount (); if (_isRetired) ret = retiredAmount (); volver ret; };
Charbel
66
@Charbel Eso no es lo mismo. Si _isSeparatedy _isRetiredpuede ser tanto verdadera (y por qué no sería eso posible?) Que devuelva el importe incorrecto.
hvd
2
@Konchog " las condiciones anidadas proporcionarán un mejor tiempo de ejecución de las cláusulas de guardia " Esto mayormente necesita una citación. Tengo mis dudas de que sea confiable en absoluto. En este caso, por ejemplo, ¿en qué se diferencian los retornos tempranos del cortocircuito lógico en términos de código generado? Incluso si importara, no puedo imaginar un caso en el que la diferencia sea más que una astilla infinitesimal. Entonces, está aplicando una optimización prematura al hacer que el código sea menos legible, solo para satisfacer algún punto teórico no comprobado sobre lo que cree que conduce a un código un poco más rápido. No hacemos eso aquí
underscore_d
1
@underscore_d, tienes razón. depende mucho del compilador, pero puede ocupar más espacio. Observe los dos pseudoconjuntos y es fácil ver por qué las cláusulas de protección provienen de lenguajes de alto nivel. Prueba "A" (1); branch_fail end; prueba (2); branch_fail end; prueba (3); branch_fail end; {CODE} end: return; Prueba "B" (1); branch_good next1; regreso; next1: prueba (2); branch_good next2; regreso; next2: prueba (3); branch_good next3; regreso; next3: {CODE} return;
Konchog
11

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

Anthony
fuente
Oye, ya no puedo llegar a ese enlace. ¿Por casualidad tienes una versión alojada en algún lugar todavía accesible?
Nic Hartley
Hola, QPT, buen lugar. He traído la publicación del blog y actualizado la URL anterior. ¡Debería vincularse ahora!
Anthony
Sin embargo, hay más que eso. Es mucho más fácil administrar un tiempo de ejecución preciso utilizando SESE. Los condicionales anidados a menudo se pueden refactorizar con un interruptor de todos modos. No se trata solo de si hay o no un valor de retorno.
Konchog
Si va a afirmar que no hay un estudio formal que lo respalde, le conviene vincularlo con uno que vaya en contra de él.
Mehrdad
Mehrdad, si hay un estudio formal que lo respalde, muéstrelo. Eso es todo. Insistir en la evidencia contra está cambiando la carga de la prueba.
Anthony
7

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.

oopexpert
fuente
Lo mismo se aplica a las variables. Cuáles son la alternativa al uso de construcciones de flujo de control como retorno temprano.
Deduplicador
Las variables en su mayoría no le impedirán dividir su código en partes de manera que se mantenga el flujo de control existente. Prueba el "método de extracción". Los IDE solo pueden realizar refactorizaciones de preservación del flujo de control, ya que no pueden derivar la semántica de lo que ha escrito.
oopexpert
5

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.

user606723
fuente
66
Todas las construcciones estructuradas que reemplazan a los gotos se implementan en términos de goto. Por ejemplo, bucles, "if" y "case". Esto no los hace malos, de hecho, todo lo contrario. Además, se trata de "intenciones y propósitos".
Anthony
Touche, pero esto no difiere de mi punto ... Simplemente hace que mi explicación sea un poco incorrecta. Oh bien.
user606723
GOTO debe estar siempre bien siempre que (1) el objetivo esté dentro del mismo método o función y (2) la dirección avance en el código (omita algún código) y (3) el objetivo no esté dentro de otra estructura anidada (p. Ej. GOTO desde el medio de if-case hasta el medio de else-case). Si sigue estas reglas, todos los usos incorrectos de GOTO tienen un olor a código realmente fuerte, tanto visual como lógicamente.
Mikko Rantalainen
3

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.

Clasificador
fuente
3
Pasar de múltiples retornos a establecer el valor de retorno en múltiples lugares no elimina la complejidad ciclomática, solo unifica la ubicación de salida. Todos los problemas que la complejidad ciclomática puede indicar en el contexto dado permanecen. "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" Nuevamente, la lógica no cambia al unificar el retorno. Si tiene que estudiar cuidadosamente el código para comprender cómo funciona, debe ser refactorizado, punto final.
WillD