¿De dónde proviene la noción de "solo una devolución"?

994

A menudo hablo con programadores que dicen " No coloques varias declaraciones de devolución en el mismo método. " Cuando les pido que me digan las razones de por qué, todo lo que obtengo es " El estándar de codificación lo dice. "o" Es confuso. "Cuando me muestran soluciones con una única 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;

¿De qué manera este código de 50% hace que el programa sea más fácil de entender? Personalmente, lo encuentro más difícil, porque el espacio del estado acaba de aumentar por otra variable que podría haberse evitado fácilmente.

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 una devolución"? ¿Hay alguna razón histórica por la que se produjo esta convención?

    
pregunta fredoverflow 06.07.2017 - 23:54

9 respuestas

1041

"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 en las que Dijkstra advirtió.

"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 admitió múltiples entradas a funciones con la declaració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 declaración inmediatamente después de la llamada. No no significaba que una función solo debería devolver desde un lugar. Cuando se escribió 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 eran altamente propensas a errores. El uso de entradas alternativas a menudo dejaba alguna variable sin inicializar. El uso de rendimientos 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.

    
respondido por el kevin cline 14.07.2017 - 04:48
887

Esta noción de Entrada única, Salida única (SESE) proviene de idiomas con gestión explícita de recursos , como C y ensamblaje. En C, código como este perderá recursos:

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

En dichos idiomas, básicamente tienes tres opciones:

  • Replica 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 que algunos argumentan que goto tiene su lugar. Y de hecho tiene - 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 en break , return , if , while ) es mucho más fácil de seguir que el flujo de control manipulado a través del estado de las variables (porque esas variables no tienen estado cuando mira el algoritmo).

En ensamblaje es aún más extraño, porque puedes saltar a cualquier dirección en una función cuando llamas a esa función, lo que efectivamente significa que tienes un número casi ilimitado de puntos de entrada a cualquier función. (A veces, esto es útil. Tales trucos 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 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 hacer provisiones para un 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 cambio emplea RAII .) Una vez que haya hecho esto, no puede limpiar después de usted debido a una declaración return temprana, entonces, ¿cuál es probablemente el argumento más sólido en El favor de SESE ha desaparecido.

Eso deja legibilidad. Por supuesto, una función de 200 LoC con media docena de declaraciones de return esparcidas aleatoriamente no es un buen estilo de programación y no constituye un código legible. Pero tal función tampoco sería fácil de entender sin esos rendimientos prematuros.

En los idiomas donde los recursos no son o no deben administrarse 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 idiomas de hoy. En lugar de ayudar a la comprensión del código, lo dificulta.

¿Por qué los programadores de Java se apegan 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 totalmente malas), donde ahora se adhiere a ellos, no importa lo que cueste. (Al igual que la convención para definir todas sus variables al principio del alcance).

Los programadores se adhieren a todo tipo de notaciones extrañas por razones irracionales. (Declaraciones estructurales profundamente anidadas, "puntas de flecha", se consideraron, en lenguajes como Pascal, un código hermoso). La aplicación de un razonamiento lógico puro a esto parece no convencer a la mayoría de ellos para que se desvíen 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. Tú, siendo profesor de programación, lo tienes en tu mano. :)

    
respondido por el sbi 07.07.2017 - 01:12
80

Por un lado, las declaraciones de retorno únicas facilitan el registro, así como las formas de depuración que se basan en el 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 otra parte, puedes refactorizar esto en function() que llama a _function() y registra el resultado.

    
respondido por el perreal 06.07.2017 - 23:57
51

"Single Entry, Single Exit" se originó con la revolución de la Programación Estructurada de principios de la década de 1970, que se inició con la carta de Edsger W. Dijkstra al Editor " Declaración de GOTO considerada dañina ". Los conceptos detrás de la programación estructurada se detallaron en el libro clásico "Programación estructurada" de Ole Johan-Dahl, Edsger W. Dijkstra y Charles Anthony Richard Hoare.

Se requiere lectura "Declaración de GOTO considerada dañina", incluso hoy. La "Programación estructurada" está fechada, pero aún así es muy, muy gratificante, y debería estar en la parte superior de la lista de "Debe Leer" de cualquier desarrollador, muy por encima de cualquier cosa, por ejemplo, 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).

    
respondido por el John R. Strohm 09.11.2011 - 15:22
31

Siempre es fácil vincular a 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

  

Utilice cláusulas de guardia en 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;
};  
     

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

    
respondido por el Pieter B 06.07.2017 - 23:58
10

Hace un tiempo escribí una publicación de blog sobre este tema.

La conclusión es que esta regla proviene de la antigüedad de los idiomas que no tienen recolección de basura o manejo de excepciones. No hay un estudio formal que muestre que esta regla conduce a un mejor código en los idiomas modernos. Siéntase libre de ignorarlo cuando esto lleve 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 e inútil.

Esta pregunta también se hizo en Stackoverflow

    
respondido por el Anthony 23.05.2017 - 14:40
6

Una devolución facilita la refactorización. Intente realizar un "método de extracción" en el cuerpo interno de un bucle for que contenga un retorno, ruptura o continuar. Esto fallará ya que ha interrumpido su flujo de control.

El punto es: supongo que nadie finge escribir código perfecto. Por lo tanto, el código se encuentra en proceso de refactorización para ser "mejorado" y extendido. Por lo tanto, mi objetivo sería mantener mi código lo más amigable posible con la refactorización.

A menudo me enfrento al problema de que tengo que reformular las funciones completamente si contienen interruptores de flujo de control y si quiero agregar solo una pequeña funcionalidad. Esto es muy propenso a errores, ya que cambia todo el flujo de control en lugar de introducir nuevas rutas a los 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 IDE y compilador.

    
respondido por el oopexpert 16.10.2017 - 21:03
5

Considere el hecho de que múltiples declaraciones de devolución son equivalentes a tener GOTO en una sola declaración de devolución. Este es el mismo caso con las declaraciones de ruptura. Como tal, algunos, como yo, los consideran GOTO para todos los propósitos y propósitos.

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 han mencionado otros, lo siguiente es una lectura obligatoria Declaración de GOTO considerada dañina
Sin embargo, tenga en cuenta que esto se escribió en 1970, cuando los GOTO estaban muy utilizados. No todos los GOTO son dañinos y no desalentaría su uso siempre y cuando no los utilices en lugar de las construcciones normales, sino en el extraño caso de que el uso de construcciones normales sea altamente inconveniente.

Encuentro que usarlos en los casos de error en los que necesitas 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 colocar este código en una función separada para que pueda regresar antes de tiempo en lugar de usar un GOTO ... pero a veces eso también es inconveniente.

    
respondido por el user606723 10.11.2011 - 17:31
1

Complejidad ciclomática

He visto que SonarCube usa una declaración de retorno múltiple para determinar la complejidad ciclomática. Así que cuanto más se devuelvan las declaraciones, mayor será la complejidad ciclomática.

Cambio de tipo de devolución

Múltiples devoluciones significa que debemos cambiar en varios lugares de la función cuando decidimos cambiar nuestro tipo de devolución.

Salidas múltiples

Es más difícil de depurar ya que la lógica debe estudiarse cuidadosamente junto con las sentencias condicionales para comprender qué causó el valor devuelto.

Solución Refactored

La solución para múltiples declaraciones de devolución es reemplazarlas con polimorfismo y tener una única devolución después de resolver el objeto de implementación requerido.

    
respondido por el Sorter 08.10.2018 - 13:33

Lea otras preguntas en las etiquetas