¿Por qué una función debería tener un solo punto de salida? [cerrado]

97

Siempre he oído hablar de una función de punto de salida único como una mala forma de codificar porque pierde legibilidad y eficiencia. Nunca escuché a nadie discutir del otro lado.

Pensé que esto tenía algo que ver con CS, pero esta pregunta fue rechazada en cstheory stackexchange.

hexadecimal bob-omb
fuente
6
La respuesta es que no hay una respuesta que siempre sea correcta. A menudo me resulta más fácil codificar con múltiples salidas. También descubrí (al actualizar el código anterior) que modificar / extender el código era más difícil debido a esas mismas salidas múltiples. Tomar estas decisiones caso por caso es nuestro trabajo. Cuando una decisión siempre tiene una "mejor" respuesta, no es necesaria para nosotros.
JS.
1
@finnw los mods fascistas han eliminado las dos últimas preguntas, para asegurarse de que tendrán que ser respondidas una y otra vez, y otra vez
Maarten Bodewes
A pesar de la palabra "discutir" en la pregunta, realmente no creo que se trate de una pregunta basada en opiniones. Es bastante relevante para un buen diseño, etc. No veo ninguna razón para cerrarlo, pero sí.
Ungeheuer
1
Un único punto de salida simplifica la depuración, lectura, medición y ajuste del rendimiento, refactorización. Esto es objetivo y materialmente significativo. El uso de devoluciones anticipadas (después de simples comprobaciones de argumentos) crea una combinación sensata de ambos estilos. Dados los beneficios de un solo punto de salida, ensuciar su código con valores de retorno es simplemente evidencia de un programador perezoso, descuidado y descuidado, y al menos posiblemente no le gusten los cachorros.
Rick O'Shea

Respuestas:

108

Hay diferentes escuelas de pensamiento, y en gran parte se reduce a preferencias personales.

Una es que es menos confuso si hay un solo punto de salida: tiene un solo camino a través del método y sabe dónde buscar la salida. En el lado negativo, si usa sangría para representar el anidamiento, su código termina con una sangría masiva a la derecha y se vuelve muy difícil seguir todos los ámbitos anidados.

Otra es que puede verificar las condiciones previas y salir temprano al comienzo de un método, de modo que sepa en el cuerpo del método que ciertas condiciones son verdaderas, sin que todo el cuerpo del método tenga sangría a 5 millas hacia la derecha. Esto generalmente minimiza la cantidad de ámbitos de los que tiene que preocuparse, lo que hace que el código sea mucho más fácil de seguir.

Una tercera es que puede salir por cualquier lugar que desee. Esto solía ser más confuso en los viejos tiempos, pero ahora que tenemos editores y compiladores de colores de sintaxis que detectan código inalcanzable, es mucho más fácil de manejar.

Estoy de lleno en el campo del medio. Hacer cumplir un solo punto de salida es una restricción inútil o incluso contraproducente en mi humilde opinión, mientras que salir al azar en todo un método a veces puede conducir a una lógica complicada y difícil de seguir, donde se vuelve difícil ver si un fragmento de código dado será o no. ejecutado. Pero "activar" su método permite simplificar significativamente el cuerpo del método.

Jason Williams
fuente
1
El anidamiento profundo puede obviarse en el singe exitparadigma a fuerza de go todeclaraciones. Además, uno tiene la oportunidad de realizar algún posprocesamiento bajo la Erroretiqueta local de la función , lo cual es imposible con múltiples returns.
Ant_222
2
Por lo general, existe una buena solución que evita la necesidad de ir a. Prefiero 'return (Fail (...))' y poner el código de limpieza compartido en el método Fail. Esto puede requerir pasar algunos locales para permitir que se libere la memoria, etc., pero a menos que esté en un fragmento de código crítico para el rendimiento, esta suele ser una solución mucho más limpia que un goto IMO. También permite que varios métodos compartan un código de limpieza similar.
Jason Williams
Existe un enfoque óptimo basado en criterios objetivos, pero podemos estar de acuerdo en que hay escuelas de pensamiento (correctas e incorrectas) y se reduce a preferencias personales (una preferencia a favor o en contra de un enfoque correcto).
Rick O'Shea
39

Mi recomendación general es que las declaraciones de retorno deben, cuando sea práctico, ubicarse antes del primer código que tiene efectos secundarios o después del último código que tiene efectos secundarios. Consideraría algo como:

  if (! argumento) // Comprueba si no es nulo
    return ERR_NULL_ARGUMENT;
  ... procesar argumento no nulo
  si (ok)
    return 0;
  más
    return ERR_NOT_OK;

más claro que:

  int return_value;
  if (argumento) // No nulo
  {
    .. procesar argumento no nulo
    .. establecer el resultado de forma adecuada
  }
  más
    resultado = ERR_NULL_ARGUMENT;
  devolver resultado;

Si una determinada condición debería evitar que una función haga algo, prefiero regresar antes de la función en un punto por encima del punto donde la función haría cualquier cosa. Sin embargo, una vez que la función ha emprendido acciones con efectos secundarios, prefiero regresar desde abajo, para dejar en claro que se deben tratar todos los efectos secundarios.

Super gato
fuente
Su primer ejemplo, la gestión de la okvariable, me parece un enfoque de retorno único. Además, el bloque if-else se puede reescribir simplemente a:return ok ? 0 : ERR_NOT_OK;
Melebius
2
El primer ejemplo tiene un returnal principio antes de todo el código que hace todo. En cuanto al uso del ?:operador, escribirlo en líneas separadas facilita en muchos IDE adjuntar un punto de interrupción de depuración al escenario no correcto. Por cierto, la clave real del "punto de salida único" radica en comprender que lo que importa es que para cada llamada en particular a una función normal, el punto de salida es el punto inmediatamente después de la llamada . Los programadores hoy en día dan eso por sentado, pero las cosas no siempre fueron así. En algunos casos raros, el código puede tener que funcionar sin espacio de pila, lo que lleva a funciones ...
supercat
... que salen a través de gotos condicionales o calculados. En general, cualquier cosa con suficientes recursos para ser programada en cualquier otra cosa que no sea el lenguaje ensamblador podrá admitir una pila, pero he escrito código ensamblador que tuvo que operar bajo algunas restricciones muy estrictas (hasta CERO bytes de RAM en un caso), y tener múltiples puntos de salida puede ser útil en tales casos.
supercat
1
El llamado ejemplo más claro es mucho menos claro y difícil de leer. Un punto de salida siempre es más fácil de leer, más fácil de mantener y más fácil de depurar.
GuidoG
8
@GuidoG: Cualquiera de los dos patrones puede ser más legible, dependiendo de lo que aparezca en las secciones omitidas. Usando "return x;" deja claro que si se alcanza la declaración, el valor de retorno será x. Usando "resultado = x;" deja abierta la posibilidad de que algo más pueda cambiar el resultado antes de que se devuelva. Eso puede ser útil si de hecho fuera necesario cambiar el resultado, pero los programadores que examinan el código tendrían que inspeccionarlo para ver cómo puede cambiar el resultado incluso si la respuesta es "no puede".
supercat
15

El punto de entrada y salida único era el concepto original de programación estructurada frente a la codificación de espagueti paso a paso. Existe la creencia de que varias funciones de punto de salida requieren más código, ya que debe realizar una limpieza adecuada de los espacios de memoria asignados para las variables. Considere un escenario en el que la función asigna variables (recursos) y salirse de la función antes de tiempo y sin una limpieza adecuada resultaría en pérdidas de recursos. Además, construir una limpieza antes de cada salida crearía una gran cantidad de código redundante.

PSS
fuente
Eso realmente no es un problema con RAII
BigSandwich
14

Con casi cualquier cosa, se reduce a las necesidades del entregable. En "los viejos tiempos", el código espagueti con múltiples puntos de retorno invitaba a pérdidas de memoria, ya que los programadores que preferían ese método normalmente no se limpiaban bien. También hubo problemas con algunos compiladores que "perdieron" la referencia a la variable de retorno cuando la pila se extrajo durante el retorno, en el caso de regresar de un ámbito anidado. El problema más general fue el código reentrante, que intenta que el estado de llamada de una función sea exactamente el mismo que su estado de retorno. Los mutadores de oop violaron esto y el concepto fue archivado.

Hay entregables, sobre todo kernels, que necesitan la velocidad que proporcionan múltiples puntos de salida. Estos entornos normalmente tienen su propia memoria y gestión de procesos, por lo que se minimiza el riesgo de una fuga.

Personalmente, me gusta tener un solo punto de salida, ya que a menudo lo uso para insertar un punto de interrupción en la declaración de retorno y realizar una inspección de código de cómo el código determinó esa solución. Podría simplemente ir a la entrada y pasar, lo que hago con soluciones recursivas y anidadas extensivamente. Como revisor de código, múltiples retornos en una función requieren un análisis mucho más profundo, por lo que si lo está haciendo para acelerar la implementación, le está robando a Peter para salvar a Paul. Se requerirá más tiempo en la revisión del código, invalidando la presunción de implementación eficiente.

- 2 centavos

Consulte este documento para obtener más detalles: NISTIR 5459

sscheider
fuente
8
multiple returns in a function requires a much deeper analysissolo si la función ya es enorme (> 1 pantalla); de lo contrario, facilita el análisis
dss539
2
múltiples devoluciones nunca facilitan el análisis, solo lo contrario
GuidoG
1
el enlace está muerto (404).
fusi
1
@fusi - lo encontré en archive.org y actualicé el enlace aquí
sscheider
4

En mi opinión, el consejo de salir de una función (u otra estructura de control) en un solo punto a menudo está sobrevendido. Por lo general, se dan dos razones para salir en un solo punto:

  1. El código de salida única es supuestamente más fácil de leer y depurar. (Admito que no creo mucho en esta razón, pero se da. Lo que es sustancialmente más fácil de leer y depurar es el código de entrada única ).
  2. El código de salida única enlaza y devuelve de forma más limpia.

La segunda razón es sutil y tiene algún mérito, especialmente si la función devuelve una gran estructura de datos. Sin embargo, no me preocuparía demasiado por eso, excepto ...

Si es estudiante, desea obtener las mejores calificaciones en su clase. Haz lo que prefiera el instructor. Probablemente tenga una buena razón desde su perspectiva; así, como mínimo, aprenderás su perspectiva. Esto tiene valor en sí mismo.

Buena suerte.

thb
fuente
4

Solía ​​ser un defensor del estilo de salida única. Mi razonamiento provino principalmente del dolor ...

La salida única es más fácil de depurar.

Dadas las técnicas y herramientas que tenemos hoy, esta es una posición mucho menos razonable para tomar, ya que las pruebas unitarias y el registro pueden hacer que la salida única sea innecesaria. Dicho esto, cuando necesita ver la ejecución de código en un depurador, es mucho más difícil de entender y trabajar con código que contiene múltiples puntos de salida.

Esto se volvió especialmente cierto cuando necesitaba interponer asignaciones para examinar el estado (reemplazado por expresiones de observación en los depuradores modernos). También fue demasiado fácil alterar el flujo de control de manera que ocultara el problema o rompiera la ejecución por completo.

Los métodos de salida única eran más fáciles de recorrer en el depurador y más fáciles de separar sin romper la lógica.

Michael Murphree
fuente
0

La respuesta depende mucho del contexto. Si está creando una GUI y tiene una función que inicializa las API y abre ventanas al comienzo de su principal, estará llena de llamadas que pueden arrojar errores, cada uno de los cuales provocaría el cierre de la instancia del programa. Si usó declaraciones IF anidadas y sangría, su código podría volverse rápidamente muy sesgado hacia la derecha. Devolver un error en cada etapa puede ser mejor y, en realidad, más legible, al mismo tiempo que es igual de fácil de depurar con algunos indicadores en el código.

Sin embargo, si está probando diferentes condiciones y devolviendo diferentes valores según los resultados en su método, puede ser una práctica mucho mejor tener un solo punto de salida. Solía ​​trabajar en scripts de procesamiento de imágenes en MATLAB, que podían volverse muy grandes. Múltiples puntos de salida pueden hacer que el código sea extremadamente difícil de seguir. Las declaraciones de cambio eran mucho más apropiadas.

Lo mejor que puede hacer es aprender sobre la marcha. Si está escribiendo código para algo, intente buscar el código de otras personas y ver cómo lo implementan. Decide qué bits te gustan y cuáles no.

Flash_Steel
fuente
-5

Si siente que necesita varios puntos de salida en una función, la función es demasiado grande y está haciendo demasiado.

Recomendaría leer el capítulo sobre funciones en el libro de Robert C. Martin, Código limpio.

Esencialmente, debería intentar escribir funciones con 4 líneas de código o menos.

Algunas notas del blog de Mike Long :

  • La primera regla de funciones: deben ser pequeñas
  • La segunda regla de funciones: deben ser más pequeñas que eso
  • Los bloques dentro de las declaraciones if, while, bucles, etc. deben tener una línea de longitud
  • ... y esa línea de código generalmente será una llamada a función
  • No debe haber más de uno o quizás dos niveles de sangría
  • Las funciones deberían hacer una cosa
  • Las declaraciones de función deben estar todas al mismo nivel de abstracción
  • Una función no debe tener más de 3 argumentos
  • Los argumentos de salida son un olor a código
  • Pasar una bandera booleana a una función es realmente horrible. Por definición, estás haciendo dos cosas en la función.
  • Los efectos secundarios son mentiras.
Roger Dahl
fuente
29
4 líneas? ¿Qué codificas que te permite tanta simplicidad? Realmente dudo que el kernel de Linux o git, por ejemplo, hagan eso.
Shinzou
7
"Pasar una bandera booleana a una función es realmente horrible. Por definición, estás haciendo dos: cosas en la función". ¿Por definición? No ... ese booleano podría potencialmente afectar solo a una de sus cuatro líneas. Además, aunque estoy de acuerdo con mantener el tamaño de la función pequeño, cuatro es un poco demasiado restrictivo. Esto debe tomarse como una guía muy flexible.
Jesse
12
Agregar restricciones como estas inevitablemente hará que el código sea confuso. Se trata más de métodos que sean limpios, concisos y que se ciñan a hacer solo lo que deben hacer sin efectos secundarios innecesarios.
Jesse
10
Una de las raras respuestas sobre SO que desearía poder rechazar varias veces.
Steven Rands
8
Desafortunadamente, esta respuesta está haciendo varias cosas y probablemente deba dividirse en muchas otras, todas de menos de cuatro líneas.
Eli