¿Debería una función tener una sola declaración de retorno?

780

¿Hay buenas razones por las que es una mejor práctica tener una sola declaración de retorno en una función?

¿O está bien regresar de una función tan pronto como sea lógicamente correcto hacerlo, lo que significa que puede haber muchas declaraciones de retorno en la función?

Tim Post
fuente
25
No estoy de acuerdo con que la pregunta sea independiente del lenguaje. Con algunos idiomas, tener retornos múltiples es más natural y conveniente que con otros. Es más probable que me queje de los primeros retornos en una función C que en una C ++ que usa RAII.
Adrian McCarthy
3
Esto está estrechamente relacionado y tiene excelentes respuestas: programmers.stackexchange.com/questions/118703/…
Tim Schmelter
¿agnóstico del lenguaje? Explique a alguien que usa lenguaje funcional que debe usar un retorno por función: p
Boiethios

Respuestas:

741

A menudo tengo varias declaraciones al comienzo de un método para regresar para situaciones "fáciles". Por ejemplo, esto:

public void DoStuff(Foo foo)
{
    if (foo != null)
    {
        ...
    }
}

... puede hacerse más legible (en mi humilde opinión) de esta manera:

public void DoStuff(Foo foo)
{
    if (foo == null) return;

    ...
}

Entonces sí, creo que está bien tener múltiples "puntos de salida" de una función / método.

Matt Hamilton
fuente
83
Convenido. Aunque tener múltiples puntos de salida puede salirse de control, definitivamente creo que es mejor que poner toda su función en un bloque IF. Use return tan a menudo como tenga sentido para mantener su código legible.
Joshua Carmody
172
Esto se conoce como una "declaración de guardia" es la refactorización de Fowler.
Lars Westergren
12
Cuando las funciones se mantienen relativamente cortas, no es difícil seguir la estructura de una función con un punto de retorno cerca del medio.
KJAWolf
21
¿Gran bloque de declaraciones if-else, cada una con un retorno? Eso no es nada. Tal cosa generalmente se refactoriza fácilmente. (por lo menos, la variación de salida única más común con una variable de resultado es, por lo que dudo que la variación de salida múltiple sea más difícil). Si desea un verdadero dolor de cabeza, observe una combinación de bucles if-else y while (controlados por locales booleanos), donde se establecen las variables de resultado, lo que lleva a un punto de salida final al final del método. Esa es la idea de la salida única enloquecida, y sí, estoy hablando por experiencia real de haber tenido que enfrentarla.
Marcus Andrén el
77
'Imagine: debe hacer el método "IncreaseStuffCallCounter" al final de la función' DoStuff '. ¿Qué vas a hacer en este caso? :) '-DoStuff() { DoStuffInner(); IncreaseStuffCallCounter(); }
Jim Balter
355

Nadie ha mencionado o citado Code Complete así que lo haré.

17.1 regreso

Minimice la cantidad de devoluciones en cada rutina . Es más difícil entender una rutina si, al leerla en la parte inferior, no eres consciente de la posibilidad de que regrese en algún lugar arriba.

Use un retorno cuando mejore la legibilidad . En ciertas rutinas, una vez que conoce la respuesta, desea devolverla a la rutina de llamada inmediatamente. Si la rutina se define de tal manera que no requiere ninguna limpieza, no volver inmediatamente significa que debe escribir más código.

Chris S
fuente
64
+1 para el matiz de "minimizar" pero no prohibir devoluciones múltiples.
Raedwald
13
"más difícil de entender" es muy subjetivo, especialmente cuando el autor no proporciona ninguna evidencia empírica para respaldar la afirmación general ... tener una sola variable que debe establecerse en muchas ubicaciones condicionales en el código para que la declaración de devolución final esté igualmente sujeta a "conscientes de la posibilidad de que la variable se le asigna algún lugar por encima int que funcione!
Heston T. Holtmann
26
Personalmente, estoy a favor de regresar temprano cuando sea posible. ¿Por qué? Bueno, cuando ve la palabra clave de retorno para un caso dado, instantáneamente sabe "He terminado": no tiene que seguir leyendo para descubrir qué ocurre, si es que algo ocurre después.
Mark Simpson
12
@ HestonT.Holtmann: Lo que hizo que Code Complete sea único entre los libros de programación fue que el consejo está respaldado por evidencia empírica.
Adrian McCarthy
99
Probablemente esta sea la respuesta aceptada, ya que menciona que tener múltiples puntos de retorno no siempre es bueno, pero a veces es necesario.
Rafid
229

Diría que sería increíblemente imprudente decidir arbitrariamente contra múltiples puntos de salida, ya que he encontrado que la técnica es útil en la práctica una y otra vez , de hecho, a menudo he refactorizado el código existente en múltiples puntos de salida para mayor claridad. Podemos comparar los dos enfoques así:

string fooBar(string s, int? i) {
  string ret = "";
  if(!string.IsNullOrEmpty(s) && i != null) {
    var res = someFunction(s, i);

    bool passed = true;
    foreach(var r in res) {
      if(!r.Passed) {
        passed = false;
        break;
      }
    }

    if(passed) {
      // Rest of code...
    }
  }

  return ret;
}

Compare esto con el código donde se permiten múltiples puntos de salida : -

string fooBar(string s, int? i) {
  var ret = "";
  if(string.IsNullOrEmpty(s) || i == null) return null;

  var res = someFunction(s, i);

  foreach(var r in res) {
      if(!r.Passed) return null;
  }

  // Rest of code...

  return ret;
}

Creo que esto último es considerablemente más claro. Hasta donde puedo decir, la crítica de múltiples puntos de salida es un punto de vista bastante arcaico en estos días.

ljs
fuente
12
La claridad está en el ojo del espectador: miro una función y busco un principio, un medio y un final. Cuando la función es pequeña, está bien, pero cuando intentas averiguar por qué algo está roto y el "resto del código" no es trivial, puedes pasar años buscando por qué lo es ret
Murph
77
En primer lugar, este es un ejemplo artificial. Segundo, ¿dónde: string ret; "ir en la segunda versión? Tercero, Ret no contiene información útil. Cuarto, ¿por qué tanta lógica en una función / método? Quinto, ¿por qué no hacer DidValuesPass (type res) luego RestOfCode () separado subfunciones?
Rick Minerich
25
@Rick 1. Según mi experiencia, no es un invento, este es en realidad un patrón que he encontrado muchas veces, 2. Está asignado en 'resto del código', tal vez eso no está claro. 3. ¿Um? Es un ejemplo? 4. Bueno, creo que se ideó en este sentido, pero es posible justificar esta cantidad, 5. podía hacer ...
ljs
55
@Rick el punto es que a menudo es más fácil regresar temprano que envolver el código en una gran declaración if. Resulta mucho en mi experiencia, incluso con una refactorización adecuada.
ljs
55
El punto de no tener múltiples declaraciones de retorno es facilitar la depuración, un segundo lugar es para facilitar la lectura. Un punto de interrupción al final le permite ver el valor de salida, sin excepciones. En cuanto a la legibilidad, las 2 funciones mostradas no se comportan igual. El retorno predeterminado es una cadena vacía si! R.Pasado pero el "más legible" lo cambia para devolver nulo. El autor interpretó mal que antes había un valor predeterminado después de solo unas pocas líneas. Incluso en ejemplos triviales es fácil tener retornos predeterminados poco claros, algo que un solo retorno al final ayuda a cumplir.
Mitch
191

Actualmente estoy trabajando en una base de código donde dos de las personas que trabajan en ella se suscriben ciegamente a la teoría del "punto único de salida" y puedo decirles que, por experiencia, es una práctica horrible horrible. Hace que el código sea extremadamente difícil de mantener y le mostraré por qué.

Con la teoría del "punto único de salida", inevitablemente terminas con un código que se ve así:

function()
{
    HRESULT error = S_OK;

    if(SUCCEEDED(Operation1()))
    {
        if(SUCCEEDED(Operation2()))
        {
            if(SUCCEEDED(Operation3()))
            {
                if(SUCCEEDED(Operation4()))
                {
                }
                else
                {
                    error = OPERATION4FAILED;
                }
            }
            else
            {
                error = OPERATION3FAILED;
            }
        }
        else
        {
            error = OPERATION2FAILED;
        }
    }
    else
    {
        error = OPERATION1FAILED;
    }

    return error;
}

Esto no solo hace que el código sea muy difícil de seguir, sino que ahora diga que debe volver y agregar una operación entre 1 y 2. Debe sangrar casi toda la función de maldición, y buena suerte asegurándose de que todo sus condiciones if / else y llaves se combinan correctamente.

Este método hace que el mantenimiento del código sea extremadamente difícil y propenso a errores.

17 de 26
fuente
55
@Murph: No tienes forma de saber que nada más ocurre después de cada condición sin leer cada una. Normalmente diría que este tipo de temas son subjetivos, pero esto es claramente incorrecto. Con un retorno en cada error, ya está, sabe exactamente lo que sucedió.
GEOCHET
66
@Murph: He visto este tipo de código utilizado, abusado y sobreutilizado. El ejemplo es bastante simple, ya que no hay verdadero if / else dentro. Todo lo que este tipo de código necesita descomponer es un "otro" olvidado. AFAIK, este código tiene una gran necesidad de excepciones.
paercebal
15
Puede refactorizarlo en esto, mantiene su "pureza": if (! SUCCEEDED (Operation1 ())) {} else error = OPERATION1FAILED; if (error! = S_OK) {if (SUCCEEDED (Operation2 ())) {} else error = OPERATION2FAILED; } if (error! = S_OK) {if (SUCCEEDED (Operation3 ())) {} else error = OPERATION3FAILED; } // etc.
Joe Pineda
66
Este código no tiene un solo punto de salida: cada una de esas declaraciones "error =" está en una ruta de salida. No se trata solo de salir de las funciones, se trata de salir de cualquier bloque o secuencia.
Jay Bazuzi
66
No estoy de acuerdo con que el retorno único "inevitablemente" resulte en una anidación profunda. Su ejemplo se puede escribir como una función lineal simple con un solo retorno (sin gotos). Y si no puede o no puede confiar exclusivamente en RAII para la gestión de recursos, los retornos tempranos terminan causando fugas o código duplicado. Lo que es más importante, los retornos tempranos hacen que sea poco práctico afirmar condiciones posteriores.
Adrian McCarthy
72

La programación estructurada dice que solo debe tener una declaración de retorno por función. Esto es para limitar la complejidad. Muchas personas, como Martin Fowler, argumentan que es más simple escribir funciones con múltiples declaraciones de retorno. Presenta este argumento en el clásico libro de refactorización que escribió. Esto funciona bien si sigues sus otros consejos y escribes pequeñas funciones. Estoy de acuerdo con este punto de vista y solo los puristas estrictos de programación estructurada se adhieren a declaraciones de retorno individuales por función.

cdv
fuente
44
La programación estructurada no dice nada. Algunas (pero no todas) personas que se describen a sí mismas como defensores de la programación estructurada dicen eso.
wnoise
15
"Esto funciona bien si sigues sus otros consejos y escribes pequeñas funciones". Este es el punto mas importante. Las funciones pequeñas rara vez necesitan muchos puntos de retorno.
66
@wnoise +1 por el comentario, tan cierto. Toda la "estructura de programación" dice que no use GOTO.
paxos1977
1
@ceretullis: a menos que sea necesario. Por supuesto, no es esencial, pero puede ser útil en C. El núcleo de Linux lo usa, y por buenas razones. GOTO consideró que Harmful hablaba sobre el uso GOTOpara mover el flujo de control incluso cuando existía la función. Nunca dice "nunca usar GOTO".
Esteban Küber
1
"se trata de ignorar la 'estructura' del código". - No, todo lo contrario. "tiene sentido decir que deberían evitarse", no, no lo hace.
Jim Balter
62

Como Kent Beck señala cuando discute cláusulas de guardia en Patrones de implementación que hacen que una rutina tenga un único punto de entrada y salida ...

"era para evitar la confusión posible al entrar y salir de muchos lugares en la misma rutina. Tenía sentido cuando se aplicaba a FORTRAN o programas en lenguaje ensamblador escritos con una gran cantidad de datos globales donde incluso comprender qué declaraciones se ejecutaron fue un trabajo duro ... con métodos pequeños y principalmente datos locales, es innecesariamente conservador ".

Encuentro que una función escrita con cláusulas de guarda es mucho más fácil de seguir que una larga serie de if then elsedeclaraciones anidadas .

blanco
fuente
Por supuesto, "un largo grupo anidado de declaraciones if-then-else" no es la única alternativa a las cláusulas de protección.
Adrian McCarthy
@AdrianMcCarthy ¿tienes una mejor alternativa? Sería más útil que el sarcasmo.
shinzou
@kuhaku: No estoy seguro de llamarlo sarcasmo. La respuesta sugiere que es una o una situación: o cláusulas de protección o largos grupos anidados de if-then-else. Muchos (¿la mayoría?) Lenguajes de programación ofrecen muchas formas de factorizar dicha lógica además de las cláusulas de protección.
Adrian McCarthy
61

En una función que no tiene efectos secundarios, no hay una buena razón para tener más de un retorno y debe escribirlos en un estilo funcional. En un método con efectos secundarios, las cosas son más secuenciales (indexadas en el tiempo), por lo que debe escribir en un estilo imperativo, utilizando la instrucción return como comando para detener la ejecución.

En otras palabras, cuando sea posible, favorezca este estilo.

return a > 0 ?
  positively(a):
  negatively(a);

Más allá de esto

if (a > 0)
  return positively(a);
else
  return negatively(a);

Si te encuentras escribiendo varias capas de condiciones anidadas, probablemente hay una forma de refactorizar eso, usando la lista de predicados, por ejemplo. Si encuentra que sus ifs y elses están muy separados sintácticamente, es posible que desee dividirlo en funciones más pequeñas. Un bloque condicional que abarca más que una pantalla llena de texto es difícil de leer.

No hay una regla estricta y rápida que se aplique a todos los idiomas. Algo así como tener una sola declaración de devolución no hará que su código sea bueno. Pero un buen código tenderá a permitirle escribir sus funciones de esa manera.

Apocalipsis
fuente
66
+1 "Si encuentra que sus ifs y elses están muy separados sintácticamente, es posible que desee dividir eso en funciones más pequeñas".
Andres Jaan Tack
44
+1, si esto es un problema, generalmente significa que está haciendo demasiado en una sola función. Realmente me deprime que esta no sea la respuesta más votada
Matt Briggs
1
Las declaraciones de guardia tampoco tienen efectos secundarios, pero la mayoría de las personas las considerarían útiles. Por lo tanto, puede haber razones para detener la ejecución antes, incluso si no hay efectos secundarios. Esta respuesta no aborda completamente el problema en mi opinión.
Maarten Bodewes
@ MaartenBodewes-owlstead Vea "La rigidez y la pereza"
Apocalisp
43

Lo he visto en los estándares de codificación para C ++ que eran una suspensión de C, como si no tuviera RAII u otra administración automática de memoria, entonces tiene que limpiar para cada devolución, lo que significa cortar y pegar de la limpieza o un goto (lógicamente lo mismo que 'finalmente' en los lenguajes administrados), los cuales se consideran de mala forma. Si sus prácticas son utilizar punteros inteligentes y colecciones en C ++ u otro sistema de memoria automático, entonces no hay una razón sólida para ello, y se trata de la legibilidad y más de una llamada de juicio.

Pete Kirkham
fuente
Bien dicho, aunque creo que es mejor copiar las eliminaciones al intentar escribir código altamente optimizado (como el software de creación de mallas 3D complejas)
Grant Peters
1
¿Qué te hace creer eso? Si tiene un compilador con una optimización deficiente donde hay algo de sobrecarga al desreferenciar el auto_ptr, puede usar punteros simples en paralelo. Aunque sería extraño escribir código 'optimizado' con un compilador no optimizador en primer lugar.
Pete Kirkham el
Esto constituye una excepción interesante a la regla: si su lenguaje de programación no contiene algo que se llama automáticamente al final del método (como try... finallyen Java) y necesita hacer un mantenimiento de recursos, puede hacerlo con un solo volver al final de un método. Antes de hacer esto, debería considerar seriamente refactorizar el código para deshacerse de la situación.
Maarten Bodewes
@PeteKirkham ¿por qué es malo ir a la limpieza? Sí, goto se puede usar mal, pero este uso en particular no es malo.
q126y
1
@ q126y en C ++, a diferencia de RAII, falla cuando se lanza una excepción. En C, es una práctica perfectamente válida. Ver stackoverflow.com/questions/379172/use-goto-or-not
Pete Kirkham
40

Me inclino por la idea de que las declaraciones de retorno en el medio de la función son malas. Puede usar los retornos para construir algunas cláusulas de protección en la parte superior de la función y, por supuesto, decirle al compilador qué devolver al final de la función sin problemas, pero los retornos en el medio de la función pueden ser fáciles de omitir y pueden hace que la función sea más difícil de interpretar.

Joel Coehoorn
fuente
38

¿Hay buenas razones por las que es una mejor práctica tener una sola declaración de retorno en una función?

Si , hay:

  • El único punto de salida ofrece un excelente lugar para hacer valer sus condiciones posteriores.
  • A menudo es útil poder poner un punto de interrupción del depurador en el retorno al final de la función.
  • Menos devoluciones significa menos complejidad. El código lineal es generalmente más simple de entender.
  • Si tratar de simplificar una función a un solo retorno causa complejidad, entonces ese es un incentivo para refactorizar a funciones más pequeñas, más generales y más fáciles de entender.
  • Si estás en un idioma sin destructores o si no usas RAII, entonces un solo retorno reduce la cantidad de lugares que tienes que limpiar.
  • Algunos idiomas requieren un único punto de salida (p. Ej., Pascal y Eiffel).

La pregunta a menudo se plantea como una falsa dicotomía entre múltiples retornos o declaraciones if profundamente anidadas. Casi siempre hay una tercera solución que es muy lineal (sin anidamiento profundo) con un solo punto de salida.

Actualización : Aparentemente, las pautas de MISRA también promueven la salida única .

Para ser claros, no digo que siempre sea incorrecto tener retornos múltiples. Pero dadas soluciones equivalentes, hay muchas buenas razones para preferir la que tiene un solo retorno.

Adrian McCarthy
fuente
2
Otra buena razón, probablemente la mejor en estos días, para tener una sola declaración de retorno es el registro. si desea agregar el registro a un método, puede colocar una única declaración de registro que transmita lo que devuelve el método.
enor
¿Qué tan común fue la declaración FORTRAN ENTRY? Ver docs.oracle.com/cd/E19957-01/805-4939/6j4m0vn99/index.html . Y si eres aventurero, puedes registrar métodos con AOP y después del consejo
Eric Jablow
1
+1 Los primeros 2 puntos fueron suficientes para convencerme. Eso con el segundo último párrafo. No estaría de acuerdo con el elemento de registro por la misma razón que desaconsejo los condicionales anidados profundos, ya que alienta a romper la regla de responsabilidad única, que es la razón principal por la que se introdujo el polimorfismo en los OOP.
Francis Rodgers el
Solo me gustaría agregar que con C # y Code Contracts, el problema posterior a la condición no es un problema, ya que todavía puede usarlo Contract.Ensurescon múltiples puntos de devolución.
julealgon
1
@ q126y: Si está utilizando gotopara llegar a un código de limpieza común, entonces probablemente haya simplificado la función para que haya un único returnal final del código de limpieza. Entonces podría decir que resolvió el problema goto, pero yo diría que lo resolvió simplificando a uno solo return.
Adrian McCarthy
33

Tener un único punto de salida proporciona una ventaja en la depuración, ya que le permite establecer un único punto de interrupción al final de una función para ver qué valor realmente se devolverá.

Mark
fuente
66
¡BRAVO! Eres la única persona que menciona esta razón objetiva . Es la razón por la que prefiero puntos de salida individuales frente a múltiples puntos de salida. Si mi depurador pudiera establecer un punto de interrupción en cualquier punto de salida, probablemente preferiría múltiples puntos de salida. Mi opinión actual es que las personas que codifican múltiples puntos de salida lo hacen para su propio beneficio a expensas de aquellos que tienen que usar un depurador en su código (y sí, estoy hablando de todos los contribuyentes de código abierto que escriben código con múltiples puntos de salida.)
MikeSchinkel
3
SI. Estoy agregando código de registro a un sistema que se comporta de manera intermitente en la producción (donde no puedo pasar). Si el codificador anterior hubiera usado una salida única, habría sido MUCHO más fácil.
Michael Blackburn
3
Es cierto, en la depuración es útil. Pero en la práctica, en la mayoría de los casos, pude establecer el punto de interrupción en la función de llamada, justo después de la llamada, efectivamente con el mismo resultado. (Y esa posición se encuentra en la pila de llamadas, por supuesto). YMMV.
foo
Esto es a menos que su depurador proporcione una función de paso o retorno de paso (y todos y cada uno de los depuradores lo hacen hasta donde yo sé), que muestra el valor devuelto inmediatamente después de que se devuelve. Sin embargo, cambiar el valor después puede ser un poco complicado si no se asigna a una variable.
Maarten Bodewes
77
No he visto un depurador en mucho tiempo que no le permita establecer un punto de interrupción en el "cierre" del método (final, llave derecha, sea cual sea su idioma) y alcanzar ese punto de interrupción, independientemente de dónde o cuántos , los statemets devueltos están en el método Además, incluso si su función solo tiene un retorno, eso no significa que no pueda salir de la función con una excepción (explícita o heredada). Entonces, creo que este no es realmente un punto válido.
Scott Gartner
19

En general trato de tener solo un único punto de salida de una función. Hay veces, sin embargo, que hacerlo termina creando un cuerpo de función más complejo de lo necesario, en cuyo caso es mejor tener múltiples puntos de salida. Realmente tiene que ser un "juicio" basado en la complejidad resultante, pero el objetivo debe ser la menor cantidad de puntos de salida posible sin sacrificar la complejidad y la comprensibilidad.

Scott Dorman
fuente
"En general trato de tener un solo punto de salida de una función" - ¿por qué? "el objetivo debe ser la menor cantidad de puntos de salida posible" - ¿por qué? ¿Y por qué 19 personas votaron por esta falta de respuesta?
Jim Balter
@JimBalter Finalmente, todo se reduce a preferencias personales. Más puntos de salida generalmente conducen a un método más complejo (aunque no siempre) y dificultan la comprensión de alguien.
Scott Dorman el
"se reduce a preferencias personales". En otras palabras, no se puede ofrecer una razón. "Más puntos de salida generalmente conducen a un método más complejo (aunque no siempre)" - No, en realidad, no lo hacen. Dadas dos funciones que son lógicamente equivalentes, una con cláusulas de protección y otra con salida única, esta última tendrá una mayor complejidad ciclomática, que numerosos estudios muestran resultados en un código que es más propenso a errores y difícil de entender. Te beneficiaría leer las otras respuestas aquí.
Jim Balter
14

No, porque ya no vivimos en la década de 1970 . Si su función es lo suficientemente larga como para que los retornos múltiples sean un problema, es demasiado larga.

(Aparte del hecho de que cualquier función de varias líneas en un idioma con excepciones tendrá múltiples puntos de salida de todos modos).

Ben Lings
fuente
14

Mi preferencia sería una salida única a menos que realmente complique las cosas. He descubierto que, en algunos casos, múltiples puntos existentes pueden enmascarar otros problemas de diseño más significativos:

public void DoStuff(Foo foo)
{
    if (foo == null) return;
}

Al ver este código, inmediatamente preguntaría:

  • ¿Es 'foo' alguna vez nulo?
  • Si es así, ¿cuántos clientes de 'DoStuff' llaman a la función con un 'foo' nulo?

Dependiendo de las respuestas a estas preguntas, podría ser que

  1. la verificación no tiene sentido ya que nunca es cierta (es decir, debería ser una afirmación)
  2. la verificación rara vez es cierta y, por lo tanto, puede ser mejor cambiar esas funciones específicas de la persona que llama, ya que de todos modos probablemente deberían tomar alguna otra acción.

En los dos casos anteriores, el código probablemente puede ser modificado con una afirmación para garantizar que 'foo' nunca sea nulo y que las personas que llaman relevantes cambien.

Hay otras dos razones (creo que específicas para el código C ++) donde existen múltiples pueden tener un efecto negativo efecto . Son el tamaño del código y las optimizaciones del compilador.

Un objeto C ++ que no sea POD en alcance a la salida de una función tendrá su destructor llamado. Cuando hay varias declaraciones de devolución, puede darse el caso de que haya diferentes objetos en el alcance y, por lo tanto, la lista de destructores para llamar será diferente. Por lo tanto, el compilador necesita generar código para cada declaración de retorno:

void foo (int i, int j) {
  A a;
  if (i > 0) {
     B b;
     return ;   // Call dtor for 'b' followed by 'a'
  }
  if (i == j) {
     C c;
     B b;
     return ;   // Call dtor for 'b', 'c' and then 'a'
  }
  return 'a'    // Call dtor for 'a'
}

Si el tamaño del código es un problema, entonces puede ser algo que valga la pena evitar.

El otro problema se relaciona con la "Optimización del valor de retorno con nombre" (también conocido como Copy Elision, ISO C ++ '03 12.8 / 15). C ++ permite que una implementación omita llamar al constructor de copias si puede:

A foo () {
  A a1;
  // do something
  return a1;
}

void bar () {
  A a2 ( foo() );
}

Simplemente tomando el código como está, el objeto 'a1' se construye en 'foo' y luego se llamará a su construcción de copia para construir 'a2'. Sin embargo, copiar elisión permite al compilador construir 'a1' en el mismo lugar en la pila que 'a2'. Por lo tanto, no es necesario "copiar" el objeto cuando la función vuelve.

Múltiples puntos de salida complican el trabajo del compilador al tratar de detectar esto, y al menos para una versión relativamente reciente de VC ++, la optimización no tuvo lugar donde el cuerpo de la función tenía múltiples retornos. Consulte Optimización del valor de retorno con nombre en Visual C ++ 2005 para obtener más detalles.

Richard Corden
fuente
1
Si elimina todo menos el último dtor de su ejemplo de C ++, el código para destruir B y luego C y B, todavía tendrá que generarse cuando finalice el alcance de la instrucción if, por lo que realmente no gana nada al no tener múltiples retornos .
Eclipse
44
+1 Y muuuy abajo en la parte inferior de la lista tenemos la verdadera razón por la que existe esta práctica de codificación : NRVO. Sin embargo, esta es una microoptimización; y, como todas las prácticas de microoptimización, probablemente fue iniciado por un "experto" de 50 años que está acostumbrado a programar en un PDP-8 de 300 kHz y no comprende la importancia del código limpio y estructurado. En general, tome el consejo de Chris S y use múltiples declaraciones de retorno siempre que tenga sentido.
BlueRaja - Danny Pflughoeft
Si bien no estoy de acuerdo con su preferencia (en mi opinión, su sugerencia de afirmación también es un punto de retorno, como lo es throw new ArgumentNullException()en C # en este caso), realmente me gustaron sus otras consideraciones, todas son válidas para mí y podrían ser críticas en algunos contextos de nicho.
julealgon
Esto está lleno de hombres de paja. La pregunta de por qué foose está probando no tiene nada que ver con el tema, que es si hacer if (foo == NULL) return; dowork; oif (foo != NULL) { dowork; }
Jim Balter
11

Tener un único punto de salida reduce la Complejidad Ciclomática y, por lo tanto, en teoría , reduce la probabilidad de que introduzcas errores en tu código cuando lo cambies. Sin embargo, la práctica tiende a sugerir que se necesita un enfoque más pragmático. Por lo tanto, tiendo a apuntar a tener un único punto de salida, pero permitir que mi código tenga varios si eso es más legible.

John Channing
fuente
Muy perspicaz. Sin embargo, creo que hasta que un programador sepa cuándo usar múltiples puntos de salida, deben limitarse a uno.
Rick Minerich
55
Realmente no. La complejidad ciclomática de "if (...) return; ... return;" es lo mismo que "if (...) {...} return;". Ambos tienen dos caminos a través de ellos.
Steve Emmerson
11

Me obligo a usar solo una returndeclaración, ya que en cierto sentido generará olor a código. Dejame explicar:

function isCorrect($param1, $param2, $param3) {
    $toret = false;
    if ($param1 != $param2) {
        if ($param1 == ($param3 * 2)) {
            if ($param2 == ($param3 / 3)) {
                $toret = true;
            } else {
                $error = 'Error 3';
            }
        } else {
            $error = 'Error 2';
        }
    } else {
        $error = 'Error 1';
    }
    return $toret;
}

(Las condiciones son arbitrarias ...)

Cuantas más condiciones, cuanto más grande sea la función, más difícil será leerla. Entonces, si está en sintonía con el olor del código, se dará cuenta y querrá refactorizar el código. Dos posibles soluciones son:

  • Retornos múltiples
  • Refactorización en funciones separadas

Devoluciones múltiples

function isCorrect($param1, $param2, $param3) {
    if ($param1 == $param2)       { $error = 'Error 1'; return false; }
    if ($param1 != ($param3 * 2)) { $error = 'Error 2'; return false; }
    if ($param2 != ($param3 / 3)) { $error = 'Error 3'; return false; }
    return true;
}

Funciones separadas

function isEqual($param1, $param2) {
    return $param1 == $param2;
}

function isDouble($param1, $param2) {
    return $param1 == ($param2 * 2);
}

function isThird($param1, $param2) {
    return $param1 == ($param2 / 3);
}

function isCorrect($param1, $param2, $param3) {
    return !isEqual($param1, $param2)
        && isDouble($param1, $param3)
        && isThird($param2, $param3);
}

Por supuesto, es más largo y un poco desordenado, pero en el proceso de refactorizar la función de esta manera, hemos

  • creó una serie de funciones reutilizables,
  • hizo la función más legible para los humanos, y
  • El enfoque de las funciones es por qué los valores son correctos.
Jrgns
fuente
55
-1: mal ejemplo. Ha omitido el manejo de mensajes de error. Si eso no fuera necesario, isCorrect podría expresarse como return xx && yy && zz; donde xx, yy y z son las expresiones isEqual, isDouble e isThird.
kauppi
10

Diría que debe tener tantos como sea necesario, o cualquiera que haga que el código sea más limpio (como las cláusulas de protección ).

Personalmente, nunca he escuchado / visto ninguna "mejor práctica" que diga que debe tener una sola declaración de devolución.

En su mayor parte, tiendo a salir de una función lo antes posible en función de una ruta lógica (las cláusulas de protección son un excelente ejemplo de esto).

Rob Cooper
fuente
10

Creo que las devoluciones múltiples suelen ser buenas (en el código que escribo en C #). El estilo de retorno único es un remanente de C. Pero probablemente no esté codificando en C.

No hay una ley que requiera solo un punto de salida para un método en todos los lenguajes de programación . Algunas personas insisten en la superioridad de este estilo, y a veces lo elevan a una "regla" o "ley", pero esta creencia no está respaldada por ninguna evidencia o investigación.

Más de un estilo de retorno puede ser un mal hábito en el código C, donde los recursos deben ser desasignados explícitamente, pero los lenguajes como Java, C #, Python o JavaScript que tienen construcciones como la recolección de basura automática y los try..finallybloques (yusing bloques en C # ), y este argumento no se aplica: en estos lenguajes, es muy poco frecuente que se necesite la desasignación de recursos manual centralizada.

Hay casos en que un solo retorno es más legible, y casos en que no lo es. Vea si reduce el número de líneas de código, aclara la lógica o reduce el número de llaves y sangrías o variables temporales.

Por lo tanto, use tantos retornos como se adapte a sus sensibilidades artísticas, porque es un problema de diseño y legibilidad, no técnico.

He hablado sobre esto con mayor detalle en mi blog .

Anthony
fuente
10

Hay cosas buenas que decir sobre tener un único punto de salida, así como hay cosas malas que decir sobre la inevitable programación de "flecha" que resulta.

Si uso múltiples puntos de salida durante la validación de entrada o la asignación de recursos, trato de poner todas las 'salidas de error' de manera muy visible en la parte superior de la función.

Tanto el artículo de Spartan Programming de "SSDSLPedia" como el artículo de punto de salida de función única de "Wiki Pattern Repository's Wiki" tienen algunos argumentos perspicaces al respecto. Además, por supuesto, hay que considerar esta publicación.

Si realmente desea un único punto de salida (en cualquier lenguaje no habilitado para excepciones), por ejemplo, para liberar recursos en un solo lugar, encuentro que la aplicación cuidadosa de goto es buena; vea, por ejemplo, este ejemplo bastante artificial (comprimido para guardar el espacio en pantalla):

int f(int y) {
    int value = -1;
    void *data = NULL;

    if (y < 0)
        goto clean;

    if ((data = malloc(123)) == NULL)
        goto clean;

    /* More code */

    value = 1;
clean:
   free(data);
   return value;
}

Personalmente, en general, no me gusta la programación de flechas más de lo que no me gustan los múltiples puntos de salida, aunque ambos son útiles cuando se aplican correctamente. Lo mejor, por supuesto, es estructurar su programa para que no lo requiera. Desglosar su función en varios fragmentos generalmente ayuda :)

Aunque al hacerlo, encuentro que de todos modos termino con múltiples puntos de salida como en este ejemplo, donde alguna función más grande se ha dividido en varias funciones más pequeñas:

int g(int y) {
  value = 0;

  if ((value = g0(y, value)) == -1)
    return -1;

  if ((value = g1(y, value)) == -1)
    return -1;

  return g2(y, value);
}

Dependiendo del proyecto o las pautas de codificación, la mayoría del código de la placa de caldera podría reemplazarse por macros. Como nota al margen, desglosarlo de esta manera hace que las funciones g0, g1, g2 sean muy fáciles de probar individualmente.

Obviamente, en un lenguaje habilitado para excepciones y OO, no usaría sentencias if como esa (o en absoluto, si pudiera salirse con la suya con poco esfuerzo), y el código sería mucho más simple. Y no flecha. Y la mayoría de los retornos no finales probablemente serían excepciones.

En breve;

  • Pocos retornos son mejores que muchos retornos
  • Más de una devolución es mejor que las flechas enormes, y las cláusulas de protección generalmente están bien.
  • Las excepciones podrían / ​​deberían reemplazar la mayoría de las 'cláusulas de protección' cuando sea posible.
Henrik Gustafsson
fuente
El ejemplo se bloquea para y <0, porque intenta liberar el puntero NULL ;-)
Erich Kitzmueller
2
opengroup.org/onlinepubs/009695399/functions/free.html "Si ptr es un puntero nulo, no se realizará ninguna acción".
Henrik Gustafsson
1
No, no se bloqueará, porque pasar NULL a free es un no-op definido. Es un concepto erróneo molestamente común que primero debe probar NULL.
hlovdal
El patrón de "flecha" no es una alternativa inevitable. Esta es una falsa dicotomía.
Adrian McCarthy
9

Conoces el dicho: la belleza está en los ojos del espectador .

Algunas personas juran por NetBeans y algunas por IntelliJ IDEA , algunas por Python y algunas por PHP .

En algunas tiendas, podría perder su trabajo si insiste en hacer esto:

public void hello()
{
   if (....)
   {
      ....
   }
}

La pregunta es sobre visibilidad y mantenibilidad.

Soy adicto al uso de álgebra booleana para reducir y simplificar la lógica y el uso de máquinas de estado. Sin embargo, había colegas anteriores que creían que mi uso de "técnicas matemáticas" en la codificación no era adecuado, porque no sería visible y mantenible. Y eso sería una mala práctica. Lo siento, las técnicas que utilizo son muy visibles y mantenibles para mí, porque cuando vuelva al código seis meses después, entendería el código claramente en lugar de ver un desorden de espagueti proverbial.

Hola amigo (como solía decir un cliente anterior) haz lo que quieras siempre y cuando sepas cómo solucionarlo cuando necesito que lo arregles.

Recuerdo que hace 20 años, un colega mío fue despedido por emplear lo que hoy se llamaría una estrategia de desarrollo ágil . Tenía un plan incremental meticuloso. Pero su gerente le gritaba: "¡No puedes liberar gradualmente funciones para los usuarios! Debes seguir con la cascada ". Su respuesta al gerente fue que el desarrollo incremental sería más preciso para las necesidades del cliente. Él creía en el desarrollo para las necesidades de los clientes, pero el gerente creía en la codificación de "requisitos del cliente".

Con frecuencia somos culpables de romper los límites de normalización de datos, MVP y MVC . Estamos en línea en lugar de construir una función. Tomamos atajos.

Personalmente, creo que PHP es una mala práctica, pero qué sé. Todos los argumentos teóricos se reducen a tratar de cumplir un conjunto de reglas.

calidad = precisión, mantenibilidad y rentabilidad.

Todas las demás reglas se desvanecen en el fondo. Y, por supuesto, esta regla nunca se desvanece:

La pereza es la virtud de un buen programador.

Beato Geek
fuente
1
"Hola amigo (como solía decir un antiguo cliente) haz lo que quieras siempre y cuando sepas cómo solucionarlo cuando necesito que lo arregles". Problema: a menudo no eres tú quien lo "arregla".
Dan Barron
Haz +1 en esta respuesta porque estoy de acuerdo con dónde vas pero no necesariamente cómo llegas allí. Yo diría que hay niveles de comprensión. es decir, la comprensión que el empleado A tiene después de 5 años de experiencia en programación y 5 años con una empresa es muy diferente a la del empleado B, un nuevo graduado universitario que acaba de comenzar con una empresa. Mi punto sería que si el empleado A es la única persona que puede entender el código, NO es mantenible y, por lo tanto, todos deberíamos esforzarnos por escribir un código que el empleado B pueda entender. Aquí es donde está el verdadero arte en el software.
Francis Rodgers
9

Me inclino hacia el uso de cláusulas de guardia para regresar temprano y, de lo contrario, salir al final de un método. La regla de entrada y salida única tiene un significado histórico y fue particularmente útil cuando se trataba de código heredado que corría a 10 páginas A4 para un único método C ++ con múltiples retornos (y muchos defectos). Más recientemente, una buena práctica aceptada es mantener los métodos pequeños, lo que hace que las salidas múltiples sean menos una impedancia para la comprensión. En el siguiente ejemplo de Kronoz copiado desde arriba, la pregunta es ¿qué ocurre en // Resto del código ... ?:

void string fooBar(string s, int? i) {

  if(string.IsNullOrEmpty(s) || i == null) return null;

  var res = someFunction(s, i);

  foreach(var r in res) {
      if(!r.Passed) return null;
  }

  // Rest of code...

  return ret;
}

Me doy cuenta de que el ejemplo es algo ingenioso, pero me vería tentado a refactorizar el bucle foreach en una declaración LINQ que luego podría considerarse una cláusula de guardia. Una vez más, en un ejemplo artificial la intención del código no es aparente y someFunction () puede tener algún otro efecto secundario o el resultado puede ser utilizado en la // Resto del código ... .

if (string.IsNullOrEmpty(s) || i == null) return null;
if (someFunction(s, i).Any(r => !r.Passed)) return null;

Dando la siguiente función refactorizada:

void string fooBar(string s, int? i) {

  if (string.IsNullOrEmpty(s) || i == null) return null;
  if (someFunction(s, i).Any(r => !r.Passed)) return null;

  // Rest of code...

  return ret;
}
David Clarke
fuente
¿C ++ no tiene excepciones? Entonces, ¿por qué regresarías en nulllugar de lanzar una excepción que indica que el argumento no es aceptado?
Maarten Bodewes
1
Como indiqué, el código de ejemplo se copia de una respuesta anterior ( stackoverflow.com/a/36729/132599 ). El ejemplo original devolvió nulos y la refactorización para lanzar excepciones de argumentos no era material hasta el punto que intentaba hacer o sobre la pregunta original. Como una buena práctica, entonces sí, normalmente (en C #) arrojaría una excepción ArgumentNullException en una cláusula de protección en lugar de devolver un valor nulo.
David Clarke
7

Una buena razón por la que puedo pensar es para el mantenimiento del código: tiene un único punto de salida. Si desea cambiar el formato del resultado, ..., es mucho más sencillo de implementar. Además, para la depuración, puede pegar un punto de interrupción allí :)

Dicho esto, una vez tuve que trabajar en una biblioteca donde los estándares de codificación imponían 'una declaración de retorno por función', y me pareció bastante difícil. Escribo muchos códigos de cómputos numéricos, y a menudo hay 'casos especiales', por lo que el código terminó siendo bastante difícil de seguir ...

OysterD
fuente
Eso realmente no hace la diferencia. Si cambia el tipo de la variable local que se devuelve, tendrá que arreglar todas las asignaciones a esa variable local. Y probablemente sea mejor definir un método con una firma diferente de todos modos, ya que también tendrá que arreglar todas las llamadas a métodos.
Maarten Bodewes
@ MaartenBodewes-owlstead: puede marcar la diferencia. Para nombrar solo dos ejemplos en los que no tendría que arreglar todas las asignaciones a la variable local o cambiar las llamadas al método, la función podría devolver una fecha como una cadena (la variable local sería una fecha real, formateada como una cadena solo en el último momento), o podría devolver un número decimal y desea cambiar el número de lugares decimales.
nnnnnn
@nnnnnn OK, si desea realizar un procesamiento posterior en la salida ... Pero simplemente generaría un nuevo método que realiza el procesamiento posterior y dejaría el anterior solo. Eso es solo un poco más difícil de refactorizar, pero de todos modos tendrá que verificar si las otras llamadas son compatibles con el nuevo formato. Pero es una razón válida, no obstante.
Maarten Bodewes
7

Múltiples puntos de salida están bien para funciones lo suficientemente pequeñas, es decir, una función que se puede ver en la longitud de una pantalla en su totalidad. Si una función larga también incluye múltiples puntos de salida, es una señal de que la función se puede cortar aún más.

Dicho esto, evito las funciones de salida múltiple a menos que sea ​​absolutamente necesario . He sentido el dolor de los errores que se deben a un retorno perdido en una línea oscura en funciones más complejas.

Jon Limjap
fuente
6

He trabajado con terribles estándares de codificación que forzaron una única ruta de salida en ti y el resultado casi siempre es un espagueti desestructurado si la función es cualquier cosa menos trivial: terminas con muchos descansos y continúa que solo se interponen en el camino.

Phil Bennett
fuente
Sin mencionar que tiene que decirle a su mente que omita la ifdeclaración en frente de cada llamada al método que devuelve éxito o no :(
Maarten Bodewes
6

El único punto de salida, todo lo demás igual, hace que el código sea mucho más legible. Pero hay una trampa: construcción popular

resulttype res;
if if if...
return res;

es falso, "res =" no es mucho mejor que "return". Tiene una sola declaración de retorno, pero múltiples puntos donde la función realmente termina.

Si tiene una función con múltiples retornos (o "res =" s), a menudo es una buena idea dividirla en varias funciones más pequeñas con un único punto de salida.

ima
fuente
6

Mi política habitual es tener solo una declaración de devolución al final de una función, a menos que la complejidad del código se reduzca considerablemente al agregar más. De hecho, soy un fanático de Eiffel, que aplica la única regla de retorno al no tener una declaración de retorno (solo hay una variable de 'resultado' creada automáticamente para colocar su resultado).

Ciertamente, hay casos en los que el código puede hacerse más claro con múltiples retornos que la versión obvia sin ellos. Se podría argumentar que se necesita más retrabajo si tiene una función que es demasiado compleja para ser comprensible sin múltiples declaraciones de retorno, pero a veces es bueno ser pragmático sobre tales cosas.

Matt Sheppard
fuente
5

Si termina con más de unas pocas devoluciones, puede haber algo mal con su código. De lo contrario, estaría de acuerdo en que a veces es bueno poder regresar de múltiples lugares en una subrutina, especialmente cuando hace que el código sea más limpio.

Perl 6: mal ejemplo

sub Int_to_String( Int i ){
  given( i ){
    when 0 { return "zero" }
    when 1 { return "one" }
    when 2 { return "two" }
    when 3 { return "three" }
    when 4 { return "four" }
    ...
    default { return undef }
  }
}

estaría mejor escrito así

Perl 6: buen ejemplo

@Int_to_String = qw{
  zero
  one
  two
  three
  four
  ...
}
sub Int_to_String( Int i ){
  return undef if i < 0;
  return undef unless i < @Int_to_String.length;
  return @Int_to_String[i]
}

Tenga en cuenta que esto es solo un ejemplo rápido

Brad Gilbert
fuente
Ok ¿Por qué fue rechazado? No es que no sea una opinión.
Brad Gilbert
5

Voto por Single return al final como guía. Esto ayuda a un manejo común de limpieza de código ... Por ejemplo, eche un vistazo al siguiente código ...

void ProcessMyFile (char *szFileName)
{
   FILE *fp = NULL;
   char *pbyBuffer = NULL:

   do {

      fp = fopen (szFileName, "r");

      if (NULL == fp) {

         break;
      }

      pbyBuffer = malloc (__SOME__SIZE___);

      if (NULL == pbyBuffer) {

         break;
      }

      /*** Do some processing with file ***/

   } while (0);

   if (pbyBuffer) {

      free (pbyBuffer);
   }

   if (fp) {

      fclose (fp);
   }
}
Alphaneo
fuente
Vota por un retorno único - en el código C. Pero, ¿qué pasaría si estuviera codificando en un idioma que tuviera recolección de basura e intente ... finalmente bloques?
Anthony
4

Esta es probablemente una perspectiva inusual, pero creo que cualquiera que crea que se deben favorecer las declaraciones de devolución múltiples nunca ha tenido que usar un depurador en un microprocesador que solo admite 4 puntos de interrupción de hardware. ;-)

Si bien los problemas del "código de flecha" son completamente correctos, un problema que parece desaparecer cuando se utilizan múltiples declaraciones de retorno es la situación en la que está utilizando un depurador. No tiene una posición general adecuada para poner un punto de interrupción para garantizar que verá la salida y, por lo tanto, la condición de retorno.

Andrew Edgecombe
fuente
55
Eso es solo un tipo diferente de optimización prematura. Nunca debe optimizar para el caso especial. Si te encuentras depurando mucho una sección específica de código, hay más errores que cuántos puntos de salida tiene.
Wedge
Depende de tu depurador también.
Maarten Bodewes
4

Cuantas más declaraciones de devolución tenga en una función, mayor será la complejidad en ese método. Si se pregunta si tiene demasiadas declaraciones de retorno, es posible que desee preguntarse si tiene demasiadas líneas de código en esa función.

Pero no, no hay nada de malo en una / varias declaraciones de devolución. En algunos lenguajes, es una mejor práctica (C ++) que en otros (C).

hazzen
fuente