¿Debo regresar temprano de una función o usar una declaración if? [cerrado]

303

A menudo escribí este tipo de función en ambos formatos, y me preguntaba si se prefiere un formato sobre otro y por qué.

public void SomeFunction(bool someCondition)
{
    if (someCondition)
    {
        // Do Something
    }
}

o

public void SomeFunction(bool someCondition)
{
    if (!someCondition)
        return;

    // Do Something
}

Por lo general, codifico con el primero, ya que esa es la forma en que funciona mi cerebro durante la codificación, aunque creo que prefiero el segundo, ya que se ocupa de cualquier manejo de errores de inmediato y me resulta más fácil de leer

Rachel
fuente
99
Llego un poco tarde a esta discusión, así que no pondré esto en una respuesta; También pensé en esto hace dos años: lecterror.com/articles/view/code-formatting-and-readability Creo que el segundo es más fácil de leer, modificar, mantener y depurar. Pero tal vez solo soy yo :)
Dr. Hannibal Lecter
44
ahora esta pregunta es un buen ejemplo de una pregunta basada en la opinión
Rudolf Olah
2
Entonces, ¿qué pasa si no hay una prueba absoluta en una u otra dirección? Cuando se proporciona suficiente argumentación en una y otra dirección y hay una votación si la respuesta es correcta, lo hace muy útil. Encuentro que preguntas como esta son perjudiciales para el valor de este sitio.
gsf
99
Y me gustan las preguntas y respuestas basadas en opiniones. Me dicen qué prefiere la mayoría y eso me permite escribir el código para que otros lo lean.
Zygimantas

Respuestas:

401

Prefiero el segundo estilo. Primero, elimine los casos no válidos, ya sea simplemente saliendo o generando excepciones según corresponda, coloque una línea en blanco allí, luego agregue el cuerpo "real" del método. Me resulta más fácil de leer.

Mason Wheeler
fuente
77
Smalltalk llama a estas "cláusulas de guardia". Al menos eso es lo que Kent Beck los llama en los patrones de mejores prácticas de Smalltalk; No sé si es un lenguaje común.
Frank Shearar
154
Salir temprano te permite sacar cosas de tu pila mental limitada. :)
Joren
50
Anteriormente escuché que esto se conoce como "el patrón de la gorila": elimine los casos graves antes de que entren por la puerta.
RevBingo
14
Incluso iría tan lejos y diría que esto definitivamente es lo ÚNICO correcto.
Oliver Weiler
38
Además, no sigue agregando sangrías si se deshace de los casos fronterizos al principio.
doppelgreener
170

Definitivamente el último. El primero no se ve mal en este momento, pero cuando obtienes un código más complejo, no puedo imaginar que alguien piense esto:

public int SomeFunction(bool cond1, string name, int value, AuthInfo perms)
{
    int retval = SUCCESS;
    if (someCondition)
    {
        if (name != null && name != "")
        {
            if (value != 0)
            {
                if (perms.allow(name)
                {
                    // Do Something
                }
                else
                {
                    reval = PERM_DENY;
                }
            }
            else
            {
                retval = BAD_VALUE;
            }
        }
        else
        {
            retval = BAD_NAME;
        }
    }
    else
    {
        retval = BAD_COND;
    }
    return retval;
}

es más legible que

public int SomeFunction(bool cond1, string name, int value, AuthInfo perms)
{
    if (!someCondition)
        return BAD_COND;

    if (name == null || name == "")
        return BAD_NAME;

    if (value == 0)
        return BAD_VALUE;

    if (!perms.allow(name))
        return PERM_DENY;

    // Do something
    return SUCCESS;
}

Admito completamente que nunca entendí la ventaja de los puntos de salida individuales.

Jason Viers
fuente
20
La ventaja de un solo punto de salida es que ... ¡hay un único punto de salida! Con su ejemplo, hay varios puntos que podrían regresar. Con una función más compleja, eso podría convertirse en un punto de salida cuando el formato del valor de retorno cambia. Por supuesto, hay momentos en que forzar un único punto de salida no tiene sentido.
JohnL
71
Las grandes funciones de @JohnL son el problema, no múltiples puntos de salida. A menos que esté trabajando en un contexto donde una llamada de función adicional ralentizará inmensamente su código, por supuesto ...
Dan Rosenstark
44
@Yar: Cierto, pero el punto se mantiene. No voy a tratar de convertir a nadie, y solo estaba señalando la ventaja de ir por la menor cantidad de puntos de salida posible (y el ejemplo de Jason fue un poco como un hombre de paja de todos modos). Justo la próxima vez que se está arrancando el pelo tratando de descubrir por qué SomeFunction a veces devuelve valores impares, tal vez desearía poder agregar una llamada de registro justo antes de la devolución. ¡Mucho más fácil de depurar si solo hay uno de los insectores! :)
JohnL
76
@Jason Viers Como si un solo return value;ayuda!?! Entonces uno tiene que cazar media docena value = ..., con la desventaja de que nunca está seguro de que el valor no se cambiará entre esta asignación y el rendimiento final. Al menos un retorno inmediato es claro que ya nada cambiará el resultado.
Sjoerd
55
@Sjoed: segundo Sjoerd aquí. Si desea registrar el resultado, puede hacerlo en el sitio de la persona que llama. Si desea saber el motivo, debe iniciar sesión en cada punto de salida / asignación para que ambos sean idénticos en este aspecto.
Matthieu M.
32

Depende : en general, no voy a salir de mi camino para tratar de mover un montón de código para romper la función antes, el compilador generalmente se encargará de eso por mí. Sin embargo, dicho esto, si hay algunos parámetros básicos en la parte superior que necesito y no puedo continuar de otra manera, comenzaré temprano. Del mismo modo, si una condición genera un ifbloque gigante en la función, también tendré una ruptura temprana como resultado de eso.

Dicho esto, sin embargo, si una función requiere algunos datos cuando se llama, generalmente voy a lanzar una excepción (ver ejemplo) en lugar de simplemente regresar.

public int myFunction(string parameterOne, string parameterTwo) {
  // Can't work without a value
  if (string.IsNullOrEmpty(parameterOne)) {
    throw new ArgumentNullException("parameterOne");
  } 
  if (string.IsNullOrEmpty(parameterTwo)) {
    throw new ArgumentNullException("parameterTwo");
  }

  // ...      
  // Do some work
  // ...

  return value;
}
rjzii
fuente
99
Y si el resultado final es mantenible, ¿a quién le importa qué estilo fue seleccionado?
Jeff Siver el
3
@Jeff Siver: por lo tanto, esto tiende a ser una pregunta de estilo de "guerra santa", al final del día se trata de preferencias personales y de lo que diga una guía de estilo interna.
rjzii
1
La conclusión clave aquí es que está lanzando una excepción en lugar de regresar temprano. El valor de retorno no debe volver a utilizarse para las comprobaciones de validez. ¿Qué pasaría si tuviera varias condiciones y quisiera que el código que usa el método sepa por qué falló? De repente, puede devolver datos comerciales reales, nada (resultado vacío) o muchas cadenas diferentes, códigos, números, ... de un solo método. Solo para describir por qué falló. No, gracias.
DanMan
¿Qué pasa con la complejidad ciclomática como sugiere mi compilador? ¿No es mejor no anidar código si se puede evitar?
l --''''''--------- '' '' '' '' '' ''
24

Prefiero el regreso temprano.

Si tiene un punto de entrada y un punto de salida, siempre tiene que rastrear todo el código en su cabeza hasta el punto de salida (nunca se sabe si algún otro código de abajo hace algo más al resultado, por lo que tiene que rastrearlo hasta que exista). Usted no hace eso, qué rama determina el resultado final. Esto es difícil de seguir.

Con una entrada y múltiples existen, regresa cuando tiene su resultado y no se molesta en rastrearlo para ver que nadie le hace nada más (porque no habrá nada más desde que regresó). Es como dividir el cuerpo del método en más pasos, cada uno con la posibilidad de devolver el resultado o dejar que el siguiente paso pruebe su suerte.

revs usuario7197
fuente
13

En la programación en C, donde tiene que limpiar manualmente, hay mucho que decir para el retorno de un punto. Incluso si no hay necesidad en este momento de limpiar algo, alguien podría editar su función, asignar algo y necesitar limpiarlo antes de regresar. Si eso sucede, será un trabajo de pesadilla revisar todas las declaraciones de devolución.

En la programación C ++ tienes destructores e incluso ahora guardias de salida de alcance. Todo esto debe estar aquí para garantizar que el código sea seguro para excepciones en primer lugar, por lo que el código está bien protegido contra la salida anticipada y, por lo tanto, hacerlo no tiene un inconveniente lógico y es puramente un problema de estilo.

No tengo suficiente conocimiento sobre Java, si se llamará "finalmente" al código de bloque y si los finalizadores pueden manejar la situación de la necesidad de garantizar que algo suceda.

C # Ciertamente no puedo responder.

El lenguaje D le brinda protectores de salida de alcance incorporados y, por lo tanto, está bien preparado para la salida temprana y, por lo tanto, no debe presentar un problema que no sea el estilo.

Las funciones, por supuesto, no deberían ser tan largas en primer lugar, y si tiene una gran declaración de cambio, su código probablemente también esté mal factorizado.

CashCow
fuente
1
C ++ permite algo llamado optimización del valor de retorno que permite que el compilador omita esencialmente la operación de copia que normalmente ocurriría cuando devuelve un valor. Sin embargo, bajo varias condiciones, eso es difícil de hacer, y los valores de retorno múltiples es uno de esos casos. En otras palabras, el uso de múltiples valores de retorno en C ++ puede hacer que su código sea más lento. Esto ciertamente se mantiene en MSC, y dado que sería bastante complicado (si no imposible) implementar RVO con múltiples valores de retorno posibles, es probable que sea un problema en todos los compiladores.
Eamon Nerbonne
1
En C, solo use gotoy, posiblemente, un retorno de dos puntos. Ejemplo (el formato de código no es posible en los comentarios):foo() { init(); if (bad) goto err; bar(); if (bad) goto err; baz(); return 0; err: cleanup(); return 1; }
mirabilos
1
En lugar de un goto, prefiero el "Método de extracción". Cuando cree que necesita implementar una variable de valor de retorno o un goto, en un esfuerzo por garantizar que siempre se llame a su código de limpieza, es un olor que debe dividir en múltiples funciones. Esto le permite simplificar su código condicional complejo utilizando cláusulas de protección y otras técnicas de devolución anticipada, al tiempo que garantiza que su código de limpieza siempre se ejecute.
BrandonLWhite
"alguien podría editar su función": esta es una explicación tan ridícula para la toma de decisiones. Cualquiera puede hacer cualquier cosa en el futuro. No significa que deba hacer algo específico hoy para evitar que alguien rompa cosas en el futuro.
Victor Yarema el
Se llama hacer que su código sea mantenible. En el mundo real, el código está escrito para fines comerciales y, a veces, un desarrollador más tarde necesita cambiarlo. En el momento en que escribí esa respuesta, aunque había parcheado CURL para introducir el almacenamiento en caché y recordar el desorden de espagueti que realmente necesitaba una reescritura adecuada en C ++ moderno
CashCow
9

Regresos anticipados para ganar. Pueden parecer feos, pero mucho menos feos que los grandes ifenvoltorios, especialmente si hay varias condiciones para verificar.

STW
fuente
9

Yo uso ambos.

Si DoSomethinghay 3-5 líneas de código, entonces el código se ve hermoso usando el primer método de formateo.

Pero si tiene muchas más líneas que eso, entonces prefiero el segundo formato. No me gusta cuando los corchetes de apertura y cierre no están en la misma pantalla.

azheglov
fuente
2
Hermoso de ninguna manera! Demasiada sangría!
JimmyKane
@JimmyKane Hay solo una sangría que puede suceder en 3-5 líneas, especialmente porque necesita 2 (?) Líneas por nivel, el resto se sangra: una para la estructura de control y el inicio del bloque, una para el final del bloque.
Deduplicador
8

Una razón clásica para la entrada única-salida única es que, de lo contrario, la semántica formal se vuelve indescriptiblemente fea (la misma razón por la que GOTO se consideró perjudicial).

Dicho de otra manera, es más fácil razonar cuándo su software saldrá de la rutina si solo tiene 1 retorno. Lo cual también es un argumento contra las excepciones.

Por lo general, minimizo el enfoque de retorno temprano.

Paul Nathan
fuente
pero para una herramienta de análisis formal, puede sintetizar una función externa con la semántica que necesita la herramienta y mantener el código legible por humanos.
Tim Williscroft
@Tim. Eso depende de la cantidad de cenas que quieras hacer y de lo que estés analizando. Encuentro SESE bastante legible si el codificador estaba cuerdo acerca de las cosas.
Paul Nathan
Mi actitud hacia el análisis está determinada por las optimizaciones del proyecto Self. El 99% de las llamadas a métodos dinámicos se pueden resolver y eliminar de forma estática. entonces puedes analizar un código bastante lineal. Como programador que trabaja, puedo asumir de manera confiable que la mayoría del código en el que trabajaré fue escrito por programadores promedio. Entonces no escribirán un código muy bueno.
Tim Williscroft
@Tim: bastante justo. Aunque me siento obligado a señalar que los lenguajes estáticos todavía tienen mucho juego.
Paul Nathan
Una razón que no tiene en cuenta las excepciones. Es por eso que la salida única es excelente en C, pero no te compra nada en C ++.
Peter
7

Personalmente, prefiero hacer comprobaciones de condición de aprobado / reprobado al principio. Eso me permite agrupar la mayoría de las fallas más comunes en la parte superior de la función con el resto de la lógica a seguir.

Nathan
fuente
6

Depende.

Regreso anticipado si hay alguna condición obvia de callejón sin salida para verificar de inmediato que haría que ejecutar el resto de la función no tenga sentido. *

Establezca Retval + retorno único si la función es más compleja y de lo contrario podría tener múltiples puntos de salida (problema de legibilidad).

* Esto a menudo puede indicar un problema de diseño. Si encuentra que muchos de sus métodos necesitan verificar algún estado externo / paramater o similar antes de ejecutar el resto del código, es probable que sea algo que debe manejar la persona que llama.

Mesas Bobby
fuente
66
Cuando escribo código que se puede compartir, mi mantra es "No asumas nada. No confíes en nadie". Siempre debe validar sus entradas y cualquier estado externo del que dependa. Es mejor lanzar una excepción que posiblemente corromper algo porque alguien le dio datos incorrectos.
TMN
@ TMN: Buen punto.
Bobby Tables
2
El punto clave aquí es que en OO lanzas una excepción , no un retorno . Los retornos múltiples pueden ser malos, los lanzamientos de excepciones múltiples no son necesariamente un olor a código.
Michael K
2
@MichaelK: Un método debería ser una excepción si no se puede cumplir la condición posterior. En algunos casos, un método debería salir temprano porque la condición posterior se logró incluso antes de que comenzara la función. Por ejemplo, si se invoca un método de "Establecer etiqueta de control" para cambiar la etiqueta de un control, la etiqueta de Fredla ventana ya está activada Fred, y establecer el nombre del control en su estado actual forzaría un redibujo (que, aunque potencialmente útil en algunos casos, podría ser molesto en el que está a la mano), sería perfectamente razonable tener la salida anticipada del método set-name si los nombres antiguos y nuevos coinciden.
supercat
3

Use un If

En el libro de Don Knuth sobre GOTO, leí que da una razón para que siempre tenga la condición más probable primero en una declaración if. Bajo el supuesto de que esta sigue siendo una idea razonable (y no por pura consideración de la velocidad de la era). Diría que los retornos tempranos no son una buena práctica de programación, especialmente teniendo en cuenta el hecho de que la mayoría de las veces no se usan para el manejo de errores, a menos que su código tenga más probabilidades de fallar que no fallar :-)

Si sigues el consejo anterior, deberías colocar ese retorno en la parte inferior de la función, y entonces es posible que ni siquiera lo llames un retorno allí, solo configura el código de error y devuelve dos líneas por lo tanto. De este modo logrando la entrada 1 salida 1 ideal.

Delphi Específico ...

Creo que esta es una buena práctica de programación para los programadores de Delphi, aunque no tengo ninguna prueba. Pre-D2009, no tenemos una forma atómica para devolver un valor, tenemos exit;y result := foo;ni tan sólo pudiéramos lanzar excepciones.

Si tuvieras que sustituir

if (true) {
 return foo;
} 

para

if true then 
begin
  result := foo; 
  exit; 
end;

es posible que se canse de ver eso en la parte superior de cada una de sus funciones y prefiera

if false then 
begin
  result := bar;

   ... 
end
else
   result := foo;

y solo evita por exitcompleto.

Peter Turner
fuente
2
Con los nuevos Delphis, podría abreviarse como if true then Exit(foo);utilizo a menudo la técnica a inicializar primero resulta nil, o FALSE, respectivamente, después de comprobar todas las condiciones de error y apenas Exit;si se ha cumplido. El caso de éxito resultse establece (típicamente) en algún lugar al final del método.
JensG
Sí, me gusta esa nueva característica, aunque parece que es solo un placer apaciguar a los programadores de Java, lo siguiente que sabes es que nos permitirán definir nuestras variables dentro de los procedimientos.
Peter Turner
Y programadores de C #. Sí, para ser honesto, lo encontraría realmente útil, ya que reduce el número de líneas entre la declaración y el uso (IIRC hay incluso alguna métrica para eso, olvidé el nombre).
JensG
2

Estoy de acuerdo con la siguiente declaración:

Personalmente soy un fanático de las cláusulas de guardia (el segundo ejemplo) ya que reduce la sangría de la función. A algunas personas no les gustan porque da como resultado múltiples puntos de retorno de la función, pero creo que es más claro con ellos.

Tomado de esta pregunta en stackoverflow .

Toto
fuente
+1 Para cláusulas de guardia. También prefiero un guardia orientado positivamente, por ejemplo: if (condition = false) return en lugar de if (!
Condition
1

Utilizo retornos tempranos casi exclusivamente en estos días, hasta el extremo. Yo escribo esto

self = [super init];

if (self != nil)
{
    // your code here
}

return self;

como

self = [super init];
if (!self)
    return;

// your code here

return self;

pero realmente no importa Si tiene más de uno o dos niveles de anidamiento en sus funciones, deben ser eliminados.

Dan Rosenstark
fuente
Estoy de acuerdo. La sangría es lo que causa problemas en la lectura. Cuanto menos sangría, mejor. Su ejemplo simple tiene el mismo nivel de sangría, pero ese primer ejemplo seguramente crecerá a más sangrías que requieren más poder cerebral.
user441521
1

Prefiero escribir:

if(someCondition)
{
    SomeFunction();
}
Josh K
fuente
2
eww. ¿Es eso prevalidación? ¿O está en un método de validación dedicado DoSomeFunctionIfSomeCondition?
STW
¿Cómo mantiene esto sus preocupaciones separadas?
justkt
2
Está violando la encapsulación al hacer que la implementación de la función (su lógica de dependencia) sea externa.
TMN
1
Si esto se ejecuta en un método público y SomeFunction () es un método privado en la misma clase, podría estar bien. Pero si alguien más llama a SomeFunction (), tendría que duplicar los controles allí. Me parece mejor hacer que cada método se encargue de lo que necesita para hacer su trabajo, nadie más debería saberlo.
Según Wiklander el
2
Este es definitivamente el estilo que Robert C. Martin propone en "Clean Code". Una función solo debe hacer una cosa. Sin embargo, en "Patrones de implementación", sugiere Kent Beck, de las dos opciones propuestas en el OP, la segunda es mejor.
Scott Whitlock,
1

Como tú, generalmente escribo el primero, pero prefiero el último. Si tengo muchas comprobaciones anidadas, generalmente refactorizo ​​al segundo método.

No me gusta cómo el manejo de errores se aleja del cheque.

if not error A
  if not error B
    if not error C
      // do something
    else handle error C
  else handle error B
else handle error A

Prefiero esto:

if error A
  handle error A; return
if error B
  handle error B; return
if error C
  handle error C; return

// do something
sacudida
fuente
0

Las condiciones en la parte superior se denominan "condiciones previas". Al poner if(!precond) return;, está visualmente enumerando todas las condiciones previas.

El uso del bloque "if-else" grande puede aumentar la sobrecarga de sangría (olvidé la cita sobre sangrías de 3 niveles).

Ming-Tang
fuente
2
¿Qué? ¿No puedes hacer un regreso temprano en Java? C #, VB (.NET y 6), y aparentemente Java (lo cual asumí que sí, pero tuve que buscar porque no he usado el lenguaje en 15 años), todos permiten retornos tempranos. así que no acuses a los 'idiomas fuertemente tipados' por no tener la función. stackoverflow.com/questions/884429/…
ps2goat
-1

Prefiero mantener si las declaraciones son pequeñas.

Entonces, elegir entre:

if condition:
   line1
   line2
    ...
   line-n

y

if not condition: return

line1
line2
 ...
line-n

Elegiría lo que describiste como "regreso temprano".

Eso sí, no me importan los retornos tempranos o lo que sea, simplemente me gusta simplificar el código, acortar los cuerpos de las declaraciones if, etc.

Los anidados si y para y mientras son horribles , evítalos a toda costa.

hasen
fuente
-2

Como otros dicen, depende. Para pequeñas funciones que devuelven valores, puedo codificar devoluciones tempranas. Pero para funciones importantes, me gusta tener siempre un lugar en el código donde sé que puedo poner algo que se ejecutará antes de que regrese.

Mike Dunlavey
fuente
-2

Practico a prueba de fallas en el nivel de función. Mantiene el código consistente y limpio (para mí y aquellos con los que he trabajado). Por eso siempre regreso temprano.

Para algunas condiciones verificadas con frecuencia, puede implementar aspectos para esas verificaciones si usa AOP.

Steven Evers
fuente