Las mejores prácticas en if / return

60

Quiero saber qué se considera una mejor manera de regresar cuando tengo una ifdeclaración.

Ejemplo 1:

public bool MyFunction()
{
   // Get some string for this example
   string myString = GetString();

   if (myString == null)
   {
      return false;
   }
   else
   {
      myString = "Name " + myString;
      // Do something more here...
      return true;
   }
}

Ejemplo 2

public bool MyFunction()
{
   // Get some string for this example
   string myString = GetString();

   if (myString == null)
   {
      return false;
   }

   myString = "Name " + myString;
   // Do something more here...
   return true;
}

Como puede ver en ambos ejemplos, la función volverá, true/falsepero ¿es una buena idea poner una elsedeclaración como en el primer ejemplo o es mejor no ponerla?

sed
fuente
77
Si está comprobando solo los errores en el primer 'if', es mejor que no incluya 'else' porque los errores no deben considerarse parte de la lógica real.
Mert Akcakaya
2
personalmente me preocuparía más la función que causa los efectos secundarios, pero ¿supongo que este es solo un ejemplo mal elegido?
jk.
14
Para mí, la primera versión es como despedir a todos los estudiantes en la sala que no terminó sus tareas, y luego una vez que se han ido, diciendo al resto de los estudiantes "Ahora bien, si has terminado su tarea ...". Tiene sentido, pero es innecesario. Como el condicional ya no es realmente condicional, tiendo a soltar el else.
Nicole
1
¿Qué es el 'Haz algo más aquí' que quieres hacer? Eso podría cambiar por completo la forma en que diseña su función.
Benedicto

Respuestas:

81

El ejemplo 2 se conoce como bloque protector. Es más adecuado para devolver / lanzar una excepción temprano si algo salió mal (parámetro incorrecto o estado no válido). En el flujo lógico normal, es mejor usar el Ejemplo 1

Kemoda
fuente
+1: una buena distinción y lo que iba a responder.
Telastyn
3
@ pR0Ps tiene la misma respuesta a continuación y proporciona un ejemplo de código de por qué termina siendo un camino más limpio a seguir. +1 por la buena respuesta.
Esta pregunta se ha hecho muchas veces en SO y, aunque puede ser polémica, esta es una buena respuesta. Muchas personas que fueron entrenadas para regresar solo desde el final tienen algunos problemas con este concepto.
Bill K
2
+1. Evitar de manera inapropiada los bloqueos de protección tiende a causar código de flecha.
Brian
¿No muestran ambos ejemplos el bloqueo de guardia?
ernie
44

Mi estilo personal es usar el single ifpara los bloques de protección y el if/ elseen el código de procesamiento del método real.

En este caso, está utilizando la myString == nullcondición de protección, por lo que tendería a utilizar el ifpatrón único .

Considere el código que es un poco más complicado:

Ejemplo 1:

public bool MyFunction(myString: string){

    //guard block
    if (myString == null){
        return false;
    }
    else{
        //processing block
        myString = escapedString(myString);

        if (myString == "foo"){
            //some processing here
            return false;
        }
        else{
            myString = "Name " + myString;
            //other stuff
            return true;
        }
    }
}

Ejemplo 2

public bool MyFunction(myString: string){

    //guard block
    if (myString == null){
        return false;
    }

    //processing block
    myString = escapedString(myString);

    if (myString == "foo"){
        //some processing here
        return false;
    }
    else{
        myString = "Name " + myString;
        //other stuff
        return true;
    }
}

En el ejemplo 1, tanto el guardia como el resto del método están en la forma if/ else. Compare eso con el Ejemplo 2, donde el bloque de protección está en la ifforma única , mientras que el resto del método usa la forma if/ else. Personalmente, encuentro que el ejemplo 2 es más fácil de entender, mientras que el ejemplo 1 parece desordenado y con demasiada sangría.

Tenga en cuenta que este es un ejemplo artificial y que podría usar else ifdeclaraciones para limpiarlo, pero estoy tratando de mostrar la diferencia entre los bloques de protección y el código de procesamiento de la función real.

Un compilador decente debería generar la misma salida para ambos de todos modos. La única razón para usar uno u otro es la preferencia personal o para ajustarse al estilo del código existente.

revs pR0Ps
fuente
18
El ejemplo 2 sería aún más fácil de leer si eliminara el segundo.
briddums
18

Personalmente, prefiero el segundo método. Siento que es más corto, tiene menos sangría y es más fácil de leer.

GSto
fuente
+1 por menos sangría. Este gesto es obvio si tiene varias comprobaciones
d.raev
15

Mi práctica personal es la siguiente:

  • No me gustan las funciones con varios puntos de salida, me resultó difícil mantener y seguir, las modificaciones de código a veces rompen la lógica interna porque es inherentemente un poco descuidado. Cuando es un cálculo complejo, creo un valor de retorno al principio y lo devuelvo al final. Esto me obliga a seguir cuidadosamente cada ruta if-else, switch, etc., establecer el valor correctamente en las ubicaciones adecuadas. También dedico un poco de tiempo a decidir si establecer un valor de retorno predeterminado o dejarlo sin inicializar al comienzo. Este método también ayuda cuando cambia la lógica o el tipo de valor de retorno o el significado.

Por ejemplo:

public bool myFunction()
{
   // First parameter loading
   String myString = getString();

   // Location of "quick exits", see the second example
   // ...

   // declaration of external resources that MUST be released whatever happens
   // ...

   // the return variable (should think about giving it a default value or not) 
   // if you have no default value, declare it final! You will get compiler 
   // error when you try to set it multiple times or leave uninitialized!
   bool didSomething = false;

   try {
     if (myString != null)
     {
       myString = "Name " + myString;
       // Do something more here...

       didSomething = true;
     } else {
       // get other parameters and data
       if ( other conditions apply ) {
         // do something else
         didSomething = true;
       }
     }

     // Edit: previously forgot the most important advantage of this version
     // *** HOUSEKEEPING!!! ***

   } finally {

     // this is the common place to release all resources, reset all state variables

     // Yes, if you use try-finally, you will get here from any internal returns too.
     // As I said, it is only my taste that I like to have one, straightforward path 
     // leading here, and this works even if you don't use the try-finally version.

   }

   return didSomething;
}
  • La única excepción: "salida rápida" al inicio (o, en casos excepcionales, dentro del proceso). Si la lógica de cálculo real no puede manejar una determinada combinación de parámetros de entrada y estados internos, o si tiene una solución fácil sin ejecutar el algoritmo, no ayuda tener todo el código encapsulado (a veces profundo) en bloques. Este es un "estado excepcional", que no forma parte de la lógica central, por lo que debo salir del cálculo tan pronto como lo detecte. En este caso no hay otra rama, en condiciones normales la ejecución simplemente continúa. (Por supuesto, el "estado excepcional" se expresa mejor lanzando una excepción, pero a veces es una exageración).

Por ejemplo:

public bool myFunction()
{
   String myString = getString();

   if (null == myString)
   {
     // there is nothing to do if myString is null
     return false;
   } 

   myString = "Name " + myString;
   // Do something more here...

   // not using return value variable now, because the operation is straightforward.
   // if the operation is complex, use the variable as the previous example.

   return true;
}

La regla de "una salida" también ayuda cuando el cálculo requiere recursos externos que tiene que liberar, o establece que debe restablecer antes de abandonar la función; a veces se agregan más tarde durante el desarrollo. Con múltiples salidas dentro del algoritmo, es mucho más difícil extender todas las ramas correctamente. (Y si se pueden producir excepciones, la liberación / reinicio también se debe poner en un bloqueo final, para evitar efectos secundarios en casos excepcionales ...).

Su caso parece caer en la categoría de "salida rápida antes del trabajo real", y lo escribiría como su versión del Ejemplo 2.

Lorand Kedves
fuente
99
+1 para "No me gustan las funciones con varios puntos de salida"
Corv1nus
@LorandKedves - agregó un ejemplo - espero que no te importe.
Matthew Flynn
@MatthewFlynn bueno, si no te importa que sea lo opuesto a lo que sugerí al final de mi respuesta ;-) Continúo con el ejemplo, espero que finalmente sea bueno para los dos :-)
Lorand Kedves
@MatthewFlynn (lo siento, solo soy un bastardo malhumorado, y gracias por ayudarme a aclarar mi opinión. Espero que les guste la versión actual).
Lorand Kedves
13
De las funciones con múltiples puntos de salida y funciones con una variable de resultado mutable, la primera me parece, con mucho, la menor de dos maldades.
Jon Purdy
9

Prefiero emplear bloques de protección siempre que puedo por dos razones:

  1. Permiten una salida rápida dada alguna condición específica.
  2. Eliminan la necesidad de declaraciones complejas e innecesarias si más adelante en el código.

En términos generales, prefiero ver métodos donde la funcionalidad principal del método es clara y mínima. Los bloques de protección ayudan a que esto ocurra visualmente.

revs drekka
fuente
5

Me gusta el enfoque "Fall Through":

public bool MyFunction()
{
   string myString = GetString();

   if (myString != null)
   {
     myString = "Name " + myString;
     return true;
    }
    return false;
}

La acción tiene una condición específica, cualquier otra cosa es solo el retorno predeterminado "fall through".

Noche oscura
fuente
1

Si tengo una sola condición, no pasaría demasiado tiempo reflexionando sobre el estilo. Pero si tengo múltiples condiciones de guardia, preferiría style2

Picuture esto. Suponga que las pruebas son complejas y que realmente no desea vincularlas en una sola condición de OR para evitar la complejidad:

//Style1
if (this1 != Right)
{ 
    return;
}
else if(this2 != right2)
{
    return;
}
else if(this3 != right2)
{
    return;
}
else
{
    //everything is right
    //do something
    return;
}

versus

//Style 2
if (this1 != Right)
{ 
   return;
}
if(this2 != right2)
{
    return;
}
if(this3 != right2)
{
    return;
}


//everything is right
//do something
return;

Aquí hay dos ventajas principales

  1. Está separando el código en una sola función en dos bloques lógicos visuales: un bloque superior de validaciones (condiciones de protección) y un bloque inferior de código ejecutable.

  2. Si tiene que agregar / eliminar una condición, reduce sus posibilidades de estropear toda la escalera if-elseif-else.

Otra ventaja menor es que tiene un par de llaves menos para cuidar.

DPD
fuente
0

Esto debería ser una cuestión de lo que suena mejor.

If condition
  do something
else
  do somethingelse

se expresa mejor entonces

if condition
  do something
do somethingelse

para métodos pequeños no será una gran diferencia, pero para métodos compuestos más grandes puede hacer que sea más difícil de entender ya que no se separará adecuadamente

José Valente
fuente
3
Si un método es tan largo que la segunda forma es difícil de entender, es demasiado larga.
Kevin Cline
77
Sus dos casos solo son equivalentes si do somethingincluye una devolución. De lo contrario, el segundo código se ejecutará do somethingelseindependientemente del ifpredicado, que no es cómo funciona el primer bloque.
Adnan
También son lo que el OP explicaba en sus ejemplos. Solo siguiendo la línea de la pregunta
José Valente
0

Encuentro irritante el ejemplo 1, porque la declaración de retorno faltante al final de una función de retorno de valor desencadena inmediatamente la bandera "Espera, hay algo mal". Entonces, en casos como este, iría con el ejemplo 2.

Sin embargo, generalmente hay más involucrados, dependiendo del propósito de la función, por ejemplo, manejo de errores, registro, etc. Así que estoy con la respuesta de Lorand Kedves sobre esto, y generalmente tengo un punto de salida al final, incluso al costo de una variable de bandera adicional. Facilita el mantenimiento y las extensiones posteriores en la mayoría de los casos.

Seguro
fuente
0

Cuando su ifdeclaración siempre regresa, entonces no hay razón para usar otra cosa para el código restante en la función. Hacerlo agrega líneas adicionales y sangría adicional. Agregar código innecesario hace que sea más difícil de leer y, como todos saben, leer el código es difícil .

briddums
fuente
0

En su ejemplo, elseobviamente es innecesario, PERO ...

Cuando se pasa rápidamente por las líneas de código, los ojos suelen mirar las llaves y las hendiduras para tener una idea del flujo del código; antes de que se asienten en el código mismo. Por lo tanto, escribir elsey colocar llaves y sangría alrededor del segundo bloque de código en realidad hace que sea más rápido para el lector ver que esta es una situación "A o B", donde se ejecutará un bloque u otro. Es decir, el lector ve los corchetes y la sangría ANTES de ver el returndespués de la inicial if.

En mi opinión, este es uno de esos raros casos en los que agregar algo redundante al código en realidad lo hace MÁS legible, no menos.

Dawood ibn Kareem
fuente
0

Prefiero el ejemplo 2 porque es inmediatamente obvio que la función devuelve algo . Pero aún más que eso, preferiría regresar de un lugar, así:

public bool MyFunction()
{
    bool result = false;

    string myString = GetString();

    if (myString != nil) {
        myString = "Name " + myString;

        result = true;
    }

    return result;
}

Con este estilo puedo:

  1. Ver de inmediato que estoy devolviendo algo.

  2. Capture el resultado de cada invocación de la función con solo un punto de interrupción.

Caleb
fuente
Esto es similar a cómo abordaría el problema. La única diferencia es que usaría la variable de resultado para capturar la evaluación myString y la usaría como valor de retorno.
Chuck Conway
@ChuckConway De acuerdo, pero estaba tratando de mantener el prototipo del OP.
Caleb
0

Mi estilo personal tiende a irse

function doQuery(string) {
    if (!query(string)) {
        query("ABORT");
        return false;
    } // else
    if(!anotherquery(string) {
        query("ABORT");
        return false;
    } // else
    return true;
}

Usando las elsedeclaraciones comentadas para indicar el flujo del programa y mantenerlo legible, pero evitando sangrías masivas que pueden llegar fácilmente a través de la pantalla si hay muchos pasos involucrados.

Shadur
fuente
1
Personalmente, espero que nunca tenga que trabajar con su código.
un CVn
Si tiene varias comprobaciones, este método podría ser un poco largo. Salir de cada cláusula if es similar a usar la instrucción goto para omitir secciones del código.
Chuck Conway
Si fuera una elección entre esto y el código con las elsecláusulas incluidas, elegiría la última porque el compilador también las ignorará. Aunque dependería de else if no aumentar la sangría más que el original una vez, si lo hiciera , estaría de acuerdo con usted. Pero mi elección sería abandonarlo por elsecompleto si se permitiera ese estilo.
Mark Hurd
0

Yo escribiría

public bool SetUserId(int id)
{
   // Get user name from id
   string userName = GetNameById(id);

   if (userName != null)
   {
       // update local ID and userName
       _id = id;
       _userNameLabelText = "Name: " + userName;
   }

   return userName != null;
}

Prefiero este estilo porque es más obvio que volvería userName != null.

tia
fuente
1
La pregunta es ¿por qué prefieres ese estilo?
Caleb
... Francamente, no estoy seguro de por qué quieres la cadena en este caso, sin mencionar por qué estás agregando una prueba booleana adicional al final que realmente no necesitas por ningún motivo.
Shadur
@Shadur Soy consciente de la prueba booleana adicional y el myStringresultado no utilizado . Acabo de copiar la función de OP. Lo cambiaré por algo que parezca más real. La prueba booleana es trivial y no debería ser un problema.
tia
1
@Shadur No estamos haciendo optimización aquí, ¿verdad? Mi punto es hacer que mi código sea fácil de leer y mantener, y personalmente lo pagaría con el costo de un cheque nulo de referencia.
tia
1
@tia Me gusta el espíritu de tu código. En lugar de evaluar el nombre de usuario dos veces, capturaría la primera evaluación en una variable y la devolvería.
Chuck Conway
-1

Mi regla general es hacer un retorno if sin otra cosa (principalmente por errores) o hacer una cadena completa if-else-if sobre todas las posibilidades.

De esta manera, evito tratar de ser demasiado elegante con mi ramificación, ya que cada vez que termino haciendo esto, codifico la lógica de la aplicación en mis ifs, lo que los hace más difíciles de mantener (por ejemplo, si una enumeración es OK o ERR y escribo todos mis ifs aprovechando el hecho de que! OK <-> ERR se vuelve una molestia agregar una tercera opción a la enumeración la próxima vez).

En su caso, por ejemplo, usaría un simple if, ya que "return if null" seguramente será un patrón común y no es probable que deba preocuparse por una tercera posibilidad (además de null / not null) en el futuro.

Sin embargo, si la prueba fuera algo que hiciera una inspección más profunda del departamento sobre los datos, me equivocaría hacia un if-else completo.

hugomg
fuente
-1

Creo que hay muchas trampas en el Ejemplo 2, que en el futuro podrían conducir a un código no deseado. Primero, el foco aquí se basa en la lógica que rodea la variable 'myString'. Por lo tanto, para ser explícito, todas las pruebas de condiciones deben realizarse en un bloque de código que tenga en cuenta la lógica conocida y la predeterminada / desconocida .

¿Qué pasa si más tarde se introdujo el código involuntariamente en el ejemplo 2 que alteró significativamente la salida?

   if (myString == null)
   {
      return false;
   }

   //add some v1 update code here...
   myString = "And the winner is: ";
   //add some v2 update code here...
   //buried in v2 updates the following line was added
   myString = null;
   //add some v3 update code here...
   //Well technically this should not be hit because myString = null
   //but we already passed that logic
   myString = "Name " + myString;
   // Do something more here...
   return true;

Creo que con el elsebloque que sigue inmediatamente a la comprobación de un valor nulo, los programadores que agregaron las mejoras a las versiones futuras agregarán toda la lógica porque ahora tenemos una cadena de lógica que no fue intencionada para la regla original (que devuelve si el valor es nulo).

Presto mi creencia en gran medida a algunas de las pautas de C # en Codeplex (enlace a eso aquí: http://csharpguidelines.codeplex.com/ ) que establecen lo siguiente:

"Agregue un comentario descriptivo si se supone que el bloque predeterminado (si no) está vacío. Además, si no se llega a ese bloque, arroje una InvalidOperationException para detectar cambios futuros que pueden caer en los casos existentes. Esto garantiza un mejor código, porque se han pensado todos los caminos que puede recorrer el código ".

Creo que es una buena práctica de programación cuando se usan bloques lógicos como este tener siempre un bloque predeterminado agregado (si no, caso: predeterminado) para tener en cuenta todas las rutas de código explícitamente y no dejar el código abierto a consecuencias lógicas involuntarias.

atconway
fuente
¿Cómo no tener otro en el ejemplo dos no tiene en cuenta todos los casos? El resultado esperado ES la ejecución continua. Agregar una cláusula else en este caso solo aumenta la complejidad del método.
Chuck Conway
No estoy de acuerdo con la capacidad de no tener toda la lógica en cuestión en un bloque conciso y determinista. El foco está en manipular el myStringvalor y el código que sucede al ifbloque es la lógica resultante si la cadena! = Nulo. Por lo tanto, dado que el foco está en la lógica adicional que manipula ese valor específico , creo que debería encapsularse en un elsebloque. De lo contrario, esto abre un potencial ya que demostré que se separó involuntariamente la lógica e introdujo consecuencias no deseadas. Puede que no suceda todo el tiempo, pero esta realmente fue una pregunta de estilo de opinión de mejores prácticas .
atconway