it-swarm-es.com

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

A menudo hablo con programadores que dicen "No ponga múltiples declaraciones de retorno en el mismo método." Cuando les pido que me digan las razones, todo lo que obtengo es "La codificación el estándar lo dice. "o" Es confuso. "Cuando me muestran soluciones con una sola declaración de retorno, 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 este 50% de hinchazón de código 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 viene esta noción de "solo un retorno"? ¿Hay alguna razón histórica por la que surgió esta convención?

1077
fredoverflow

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

      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 devolver a un lugar: la instrucción que sigue inmediatamente a la llamada. Sí no significa que una función solo debería devolver de un lugar. Cuando se escribió Programación estructurada , era una práctica común que una función indicara un error volviendo 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.

1145
kevin cline

Esta noción de ¡Entrada única, salida única (SESE) proviene de ¡idiomas con gestión explícita de recursos, como C y Asamblea. 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 un 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 sostienen 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 (piensa break, return, if, while) es mucho más fácil de seguir que control de flujo manipulado a través del estado de las variables (porque esas variables no tienen estado cuando se mira el algoritmo).

En Assembly 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 ajuste de puntero this necesario para llamar a las funciones virtual 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 y using (cuando se implementa IDisposable, finally de lo contrario) en C #; C++ en su lugar emplea RAII .) Una vez que haya hecho esto, usted ¡no puede no puede limpiar después de usted debido a una declaración inicial de return, entonces, ¿cuál es probablemente el argumento más fuerte a favor de SESE?.

Eso deja legibilidad. Por supuesto, una función 200 LoC con media docena de sentencias return esparcidas aleatoriamente sobre ella no es un buen estilo de programación y no permite un 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 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 los aplicó a su OO mundo (donde son inútiles o directamente malos), donde ahora se adhiere a ellos, sin importar los costos. (Al igual que la convención para definir todo 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 el 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. :)

921
sbi

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() que llama a _function() y registra el resultado.

82
perreal

"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 perjudicial ". Los conceptos detrás de la programación estructurada se expusieron en detalle en el clásico libro "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, p. 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).

53
John R. Strohm

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

36
Pieter B

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 Java chicos que insisten en esto son ciegos e incuestionables siguiendo una regla obsoleta y sin sentido.

Esta pregunta también se ha hecho en Stackoverflow

11
Anthony

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, un corte o continúe. 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 el compilador y IDE capacidades de refactorización compatibles.

6
oopexpert

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 lectura obligada Declaración GOTO considerada dañina
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 y cuando no los use en lugar de las construcciones normales, sino en el extraño caso de que usar las 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.

5
user606723

Complejidad Ciclomática

He visto a SonarCube usar una declaración de retorno múltiple para determinar la complejidad ciclomática. Entonces, más 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 polimorfismo para obtener un solo retorno después de resolver el objeto de implementación requerido.

4
Sorter