¿Debería usarse "else" en situaciones donde el flujo de control lo hace redundante?

18

A veces me encuentro con un código similar al siguiente ejemplo (lo que esta función hace exactamente está fuera del alcance de esta pregunta):

function doSomething(value) {
  if (check1(value)) {
    return -1;
  }
  else if (check2(value)) {
    return value;
  }
  else {
    return false;
  }
}

Como se puede ver, if, else ify elselas declaraciones se utilizan en conjunción con el returncomunicado. Esto parece bastante intuitivo para un observador casual, pero creo que sería más elegante (desde la perspectiva de un desarrollador de software) eliminar los else-s y simplificar el código de esta manera:

function doSomething(value) {
  if (check1(value)) {
    return -1;
  }
  if (check2(value)) {
    return value;
  }
  return false;
}

Esto tiene sentido, ya que todo lo que sigue a una returndeclaración (en el mismo ámbito) nunca se ejecutará, haciendo que el código anterior sea semánticamente igual al primer ejemplo.

¿Cuál de los anteriores se ajusta más a las buenas prácticas de codificación? ¿Hay algún inconveniente en cualquiera de los métodos con respecto a la legibilidad del código?

Editar: se ha hecho una sugerencia duplicada con esta pregunta como referencia. Creo que mi pregunta toca un tema diferente, ya que no estoy preguntando por evitar declaraciones duplicadas como se presenta en la otra pregunta. Ambas preguntas buscan reducir las repeticiones, aunque de maneras ligeramente diferentes.

rinoceronte
fuente
14
El problema elsees menor, el mayor problema es que obviamente está devolviendo dos tipos de datos de una sola función, lo que hace que la API de la función sea impredecible.
Andy
3
E_CLEARLY_NOTADUPE
kojiro
3
@DavidPacker: al menos dos; Podrían ser tres. -1es un número, falsees un booleano y valueno se especifica aquí, por lo que podría ser cualquier tipo de objeto.
Yay295
1
@ Yay295 Esperaba valueque en realidad sea un número entero. Si puede ser algo, eso es aún peor.
Andy
@DavidPacker Este es un punto muy importante, especialmente cuando se plantea una pregunta sobre buenas prácticas. Estoy de acuerdo con usted y con Yay295, sin embargo, las muestras de código sirven como ejemplo para demostrar una técnica de codificación no relacionada. Sin embargo, tengo en cuenta que este es un asunto importante y no debe pasarse por alto en el código real.
rinoceronte

Respuestas:

23

Me gusta el que no tiene elsey he aquí por qué:

function doSomething(value) {
  //if (check1(value)) {
  //  return -1;
  //}
  if (check2(value)) {
    return value;
  }
  return false;
}

Porque hacer eso no rompió nada de lo que se suponía que no debía hacerlo.

Odio la interdependencia en todas sus formas (incluida la denominación de una función check2()). Aísle todo lo que pueda aislarse. A veces necesitas elsepero no veo eso aquí.

naranja confitada
fuente
Generalmente prefiero esto también, por la razón aquí indicada. Sin embargo, generalmente coloco un comentario al final de cada ifbloque de } //elsepara aclarar, al leer el código, que la única ejecución prevista después del ifbloque de código es si la condición en la ifdeclaración era falsa. Sin algo como esto, particularmente si el código dentro del ifbloque se vuelve más complejo, puede que no esté claro para quien está manteniendo el código que la intención era nunca ejecutar más allá del ifbloque cuando la ifcondición es verdadera.
Makyen
@Makyen planteas un buen punto. Una que estaba tratando de ignorar. También creo que se debe hacer algo después de la ifpara dejar en claro que esta sección de código está hecha y la estructura está terminada. Para hacer esto, hago lo que no he hecho aquí (porque no quería distraerme del punto principal del OP haciendo otros cambios). Hago lo más simple que puedo para lograr todo eso sin distraerme. Añado una línea en blanco.
candied_orange
27

Creo que depende de la semántica del código. Si sus tres casos dependen uno del otro, indíquelo explícitamente. Esto aumenta la legibilidad del código y hace que sea más fácil de entender para cualquier otra persona. Ejemplo:

if (x < 0) {
    return false;
} else if (x > 0) {
    return true;
} else {
    throw new IllegalArgumentException();
}

Aquí depende claramente del valor de xen los tres casos. Si omitiera el último else, esto sería menos claro.

Si sus casos no dependen directamente el uno del otro, omítalo:

if (x < 0) {
    return false;
}
if (y < 0) {
    return true;
}
return false;

Es difícil mostrar esto en un ejemplo simple sin contexto, pero espero que entiendas mi punto.

André Stannek
fuente
6

Yo prefiero la segunda opción (separar ifssin else ify principios return) pero eso es todo el tiempo que los bloques de código son cortos .

Si los bloques de código son largos, es mejor usarlo else if, porque de lo contrario no tendría una comprensión clara de los diversos puntos de salida que tiene el código.

Por ejemplo:

function doSomething(value) {
  if (check1(value)) {
    // ...
    // imagine 200 lines of code
    // ...
    return -1;
  }
  if (check2(value)) {
    // ...
    // imagine 150 lines of code
    // ...
    return value;
  }
  return false;
}

En ese caso, es mejor usarlo else if, almacenar el código de retorno en una variable y tener solo una oración de retorno al final.

function doSomething(value) {
  retVal=0;
  if (check1(value)) {
    // ...
    // imagine 200 lines of code
    // ...
    retVal=-1;
  } else if (check2(value)) {
    // ...
    // imagne 150 lines of code
    retVal=value;
  }
  return retVal;
}

Pero como uno debería esforzarse por que las funciones sean cortas, yo diría que sea breve y salga temprano como en su primer fragmento de código.

Tulains Córdova
fuente
1
Estoy de acuerdo con su premisa, pero mientras estemos discutiendo cómo debería ser el código, ¿podríamos no estar de acuerdo en que los bloques de código deberían ser cortos de todos modos? Creo que es peor tener un bloque de código largo no dividido en funciones que usar otro si, si tuviera que elegir.
kojiro
1
Un argumento curioso. returnestá dentro de 3 líneas de ifmodo que no es tan diferente que else ifincluso después de 200 líneas de código. El estilo de retorno único que presenta aquí es una tradición de c y otros lenguajes que no informan errores con excepciones. El objetivo era crear un lugar único para limpiar los recursos. Los lenguajes de excepción usan un finallybloque para eso. Supongo que esto también crea un lugar para poner un punto de interrupción si su depurador no le permite poner uno en las funciones de cierre de llaves.
candied_orange
44
No me gusta la tarea retValen absoluto. Si puede salir de una función de manera segura, hágalo de inmediato, sin asignar el valor seguro que se devolverá a una variable de una sola salida de la función. Cuando usa el punto de retorno único en una función realmente larga, generalmente no confío en otros programadores y termino buscando en el resto de la función también otras modificaciones del valor de retorno. Fallar rápido y regresar temprano son, entre otras, dos reglas por las que vivo.
Andy
2
En mi opinión, es aún peor para los bloques largos. Intento salir de las funciones lo antes posible, sin importar el contexto externo.
Andy
2
Estoy con DavidPacker aquí. El estilo de retorno único nos obliga a estudiar todos los puntos de asignación, así como el código después de los puntos de asignación. Estudiar los puntos de salida de un estilo de retorno temprano sería preferible para mí, largo o corto. Mientras el lenguaje permita un bloqueo final.
candied_orange
4

Yo uso ambos en diferentes circunstancias. En una verificación de validación, omito lo demás. En un flujo de control, uso lo demás.

bool doWork(string name) {
  if(name.empty()) {
    return false;
  }

  //...do work
  return true;
}

vs

bool doWork(int value) {
  if(value % 2 == 0) {
    doWorkWithEvenNumber(value);
  }
  else {
    doWorkWithOddNumber(value);
  }
}

El primer caso se lee más como si estuviera comprobando todos los requisitos previos primero, quitándolos del camino y luego avanzando al código real que desea ejecutar. Como tal, el código jugoso que realmente desea ejecutar pertenece directamente al alcance de la función; de eso se trata realmente la función.
El segundo caso se lee más como si ambas rutas fueran un código válido para ejecutarse que son tan relevantes para la función como el otro en función de alguna condición. Como tal, pertenecen en niveles de alcance similares entre sí.

Altainia
fuente
0

La única situación que he visto que realmente importa es un código como este:

List<?> toList() {
    if (left != null && right != null) {
        Arrays.asList(merge(left, right));
    }
    if (left != null) {
        return Arrays.asList(left);
    }
    if (right != null) {
        return Arrays.asList(right);
    }
    return Arrays.asList();
}

Claramente hay algo mal aquí. El problema es que no está claro si returndebe agregarse o Arrays.asListeliminarse. No puede solucionar esto sin una inspección más profunda de los métodos relacionados. Puede evitar esta ambigüedad utilizando siempre bloques if-else completamente exhaustivos. Esta:

List<?> toList() {
    if (left != null && right != null) {
        Arrays.asList(merge(left, right));
    } else if (left != null) {
        return Arrays.asList(left);
    } else if (right != null) {
        return Arrays.asList(right);
    } else {
        return Arrays.asList();
    }
}

no se compilará (en idiomas estáticamente controlados) a menos que agregue un retorno explícito primero if.

Algunos idiomas ni siquiera permiten el primer estilo. Intento usarlo solo en un contexto estrictamente imperativo o para condiciones previas en métodos largos:

List<?> toList() {
    if (left == null || right == null) {
        throw new IllegalStateException()
    }
    // ...
    // long method
    // ...
}
Banthar
fuente
-2

Tener tres salidas de la función como esa es realmente confuso si está tratando de seguir el código y una mala práctica.

Si tiene un único punto de salida para la función, se requieren los otros.

var result;
if(check1(value)
{
    result = -1;
}
else if(check2(value))
{
    result = value;
}
else 
{
    result = 0;
}
return result;

De hecho, vamos aún más lejos.

var result = 0;
var c1 = check1(value);
var c2 = check2(value);

if(c1)
{
    result = -1;
}
else if(c2)
{
    result = value;
}

return result;

Es justo que pueda argumentar por un solo retorno temprano de la validación de entrada. Simplemente evite envolver todo el volumen si su lógica en un if.

Ewan
fuente
3
Este estilo proviene de idiomas con gestión explícita de recursos. Se necesitaba un punto único para liberar los recursos asignados. En idiomas con gestión automática de recursos, el punto de retorno único no es tan importante. Debería concentrarse más bien en reducir el número y el alcance de las variables.
Banthar
a: no se especifica ningún idioma en la pregunta. b: Creo que te equivocas en esto. Hay muchas buenas razones para el retorno único. reduce la complejidad y mejora la legibilidad
Ewan
1
A veces reduce la complejidad, a veces la aumenta. Ver wiki.c2.com/?ArrowAntiPattern
Robert Johnson
No, creo que siempre reduce la complejidad ciclomática, vea mi segundo ejemplo. check2 siempre se ejecuta
Ewan
tener el retorno temprano es una optimización que complica el código al mover un código a un condicional
Ewan
-3

Prefiero omitir la declaración redundante else. Cada vez que lo veo (en la revisión de código o refactorización) me pregunto si el autor entendió el flujo de control y que podría haber un error en el código.

Si el autor creía que la declaración else era necesaria y, por lo tanto, no entendía las implicaciones del flujo de control de la declaración return, tal falta de comprensión es una fuente importante de errores, en esta función y en general.

Andreas Warberg
fuente
3
Si bien estoy de acuerdo contigo, esta no es una encuesta de opinión. Haga una copia de seguridad de sus reclamos o los votantes no lo tratarán amablemente.
candied_orange