Foreach-loop con break / return vs. while-loop con explícita invariante y post-condición

17

Esta es la forma más popular (me parece) de verificar si un valor está en una matriz:

for (int x : array)
{
    if (x == value)
        return true;
}
return false;        

Sin embargo, en un libro que leí hace muchos años por, probablemente, Wirth o Dijkstra, se dijo que este estilo es mejor (en comparación con un bucle while con una salida adentro):

int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

De esta manera, la condición de salida adicional se convierte en una parte explícita del bucle invariante, no hay condiciones ocultas y salidas dentro del bucle, todo es más obvio y más en una forma de programación estructurada. En general, preferí este último patrón siempre que era posible y usé el forbucle para iterar solo de aa b.

Y, sin embargo, no puedo decir que la primera versión sea menos clara. Quizás sea aún más claro y fácil de entender, al menos para los principiantes. ¿Entonces todavía me pregunto cuál es mejor?

¿Tal vez alguien puede dar una buena razón a favor de uno de los métodos?

Actualización: no se trata de puntos de retorno de funciones múltiples, lambdas o de encontrar un elemento en una matriz per se. Se trata de cómo escribir bucles con invariantes más complejos que una sola desigualdad.

Actualización: OK, veo el punto de las personas que responden y comentan: mezclé el bucle foreach aquí, que en sí mismo es mucho más claro y legible que un bucle while. No debería haber hecho eso. Pero esta también es una pregunta interesante, así que vamos a dejarlo como está: foreach-loop y una condición adicional en el interior, o while-loop con un ciclo explícito invariante y una condición posterior después. Parece que el foreach-loop con una condición y una salida / break está ganando. Crearé una pregunta adicional sin el foreach-loop (para una lista vinculada).

Danila Piatov
fuente
2
Los ejemplos de código citados aquí mezclan varios problemas diferentes. Retornos tempranos y múltiples (que para mí van al tamaño del método (no se muestra)), búsqueda de matriz (que pide una discusión que involucre lambdas), foreach vs. indexación directa ... Esta pregunta sería más clara y fácil de resolver responda si se centró solo en uno de estos problemas a la vez.
Erik Eidt
55
Posible duplicado de ¿Deben los métodos regresar siempre de un lugar?
mosquito
1
Sé que esos son ejemplos, pero hay lenguajes que tienen API para manejar exactamente ese caso de uso. Es decircollection.contains(foo)
Berin Loritsch
2
Es posible que desee encontrar el libro y releerlo ahora para ver lo que realmente dice.
Thorbjørn Ravn Andersen
1
"Mejor" es una palabra muy subjetiva. Dicho esto, uno puede decir de un vistazo lo que está haciendo la primera versión. Que la segunda versión haga exactamente lo mismo requiere cierto escrutinio.
David Hammen

Respuestas:

19

Creo que para bucles simples, como estos, la primera sintaxis estándar es mucho más clara. Algunas personas consideran confusas las devoluciones múltiples o un olor a código, pero para un fragmento de código tan pequeño, no creo que este sea un problema real.

Se vuelve un poco más discutible para bucles más complejos. Si el contenido del bucle no cabe en su pantalla y tiene varios retornos en el bucle, se debe argumentar que los múltiples puntos de salida pueden hacer que el código sea más difícil de mantener. Por ejemplo, si tuviera que asegurarse de que se ejecutara algún método de mantenimiento de estado antes de salir de la función, sería fácil no agregarlo a una de las declaraciones de retorno y causaría un error. Si todas las condiciones finales se pueden verificar en un ciclo while, solo tiene un punto de salida y puede agregar este código después.

Dicho esto, especialmente con los bucles, es bueno intentar poner tanta lógica como sea posible en métodos separados. Esto evita muchos casos en los que el segundo método tendría ventajas. Los bucles Lean con lógica claramente separada serán más importantes que los estilos que use. Además, si la mayoría de la base de código de su aplicación está usando un estilo, debe seguir con ese estilo.

Natanael
fuente
56

Esto es facil.

Casi nada importa más que la claridad para el lector. La primera variante la encontré increíblemente simple y clara.

La segunda versión 'mejorada', tuve que leer varias veces y asegurarme de que todas las condiciones de borde fueran correctas.

Hay CERO DUDA, que es un mejor estilo de codificación (el primero es mucho mejor).

Ahora, lo que está CLARO para las personas puede variar de persona a persona. No estoy seguro de que haya estándares objetivos para eso (aunque publicar en un foro como este y obtener una variedad de aportes de la gente puede ayudar).

En este caso particular, sin embargo, puedo decirle por qué el primer algoritmo es más claro: sé cómo se ve y hace la iteración de C ++ sobre una sintaxis de contenedor. Lo he internalizado. Alguien UNFAMILIAR (su nueva sintaxis) con esa sintaxis podría preferir la segunda variación.

Pero una vez que conoce y entiende esa nueva sintaxis, es un concepto básico que puede usar. Con el enfoque de iteración de bucle (segundo), debe verificar cuidadosamente que el usuario verifique CORRECTAMENTE todas las condiciones de borde para recorrer toda la matriz (por ejemplo, menos que en lugar de menor o igual, el mismo índice utilizado para la prueba y para indexar, etc.)

Lewis Pringle
fuente
44
Nuevo es relativo, como ya estaba en el estándar de 2011. Además, la segunda demostración obviamente no es C ++.
Deduplicador
Una solución alternativa si desea utilizar un único punto de salida sería establecer una bandera longerLength = true, entonces return longerLength.
Cullub
@Deduplicator ¿Por qué no es la segunda demo C ++? No veo por qué no o me estoy perdiendo algo obvio.
Rakete1111
2
@ Rakete1111 Las matrices sin formato no tienen propiedades similares length. Si en realidad se declarara como una matriz y no como un puntero, podrían usar sizeof, o si fuera una std::array, la función de miembro correcta es que size()no hay una lengthpropiedad.
IllusiveBrian
@IllusiveBrian: sizeofestaría en bytes ... El más genérico desde C ++ 17 es std::size().
Deduplicador
9
int i = 0;
while (i < array.length && array[i] != value)
    i++;
return i < array.length;

[…] Todo es más obvio y más en una forma de programación estructurada.

No exactamente. La variable iexiste fuera del bucle while aquí y, por lo tanto, es parte del alcance externo, mientras que (juego de palabras intencionado) xdel forbucle -lo existe solo dentro del alcance del bucle. El alcance es una forma muy importante de introducir estructura a la programación.

nulo
fuente
1
@ruakh No estoy seguro de qué quitar de tu comentario. Parece algo pasivo-agresivo, como si mi respuesta se opusiera a lo que está escrito en la página wiki. Por favor elabora.
nulo
"Programación estructurada" es un término de arte con un significado específico, y el OP es objetivamente correcto que la versión # 2 se ajusta a las reglas de programación estructurada mientras que la versión # 1 no lo hace. Según su respuesta, parecía que no estaba familiarizado con el término de arte y que estaba interpretando el término literalmente. No estoy seguro de por qué mi comentario parece pasivo-agresivo; Lo dije simplemente como informativo.
ruakh
@ruakh No estoy de acuerdo con que la versión # 2 se ajuste más a las reglas en todos los aspectos y lo expliqué en mi respuesta.
nulo
Dices "No estoy de acuerdo" como si fuera algo subjetivo, pero no lo es. Volver desde el interior de un bucle es una violación categórica de las reglas de la programación estructurada. Estoy seguro de que muchos entusiastas de la programación estructurada son fanáticos de las variables de alcance mínimo, pero si reduce el alcance de una variable al apartarse de la programación estructurada, entonces se ha alejado de la programación estructurada, punto, y la reducción del alcance de la variable no deshace ese.
ruakh
2

Los dos bucles tienen una semántica diferente:

  • El primer bucle simplemente responde una simple pregunta de sí / no: "¿La matriz contiene el objeto que estoy buscando?" Lo hace de la manera más breve posible.

  • El segundo bucle responde a la pregunta: "Si la matriz contiene el objeto que estoy buscando, ¿cuál es el índice de la primera coincidencia?" Nuevamente, lo hace de la manera más breve posible.

Dado que la respuesta a la segunda pregunta proporciona estrictamente más información que la respuesta a la primera, puede elegir responder la segunda pregunta y luego derivar la respuesta de la primera pregunta. Eso es lo que hace la línea return i < array.length;, de todos modos.

Creo que generalmente es mejor usar la herramienta que se ajuste al propósito a menos que pueda reutilizar una herramienta ya existente y más flexible. Es decir:

  • Usar la primera variante del bucle está bien.
  • Cambiar la primera variante para establecer una boolvariable y romper también está bien. (Evita la segunda returndeclaración, la respuesta está disponible en una variable en lugar de un retorno de función).
  • Usar std::findestá bien (¡reutilización de código!).
  • Sin embargo, codificar explícitamente un hallazgo y luego reducir la respuesta a a boolno lo es.
cmaster - restablecer monica
fuente
Sería bueno si los
votantes negativos
2

Sugeriré una tercera opción por completo:

return array.find(value);

Hay muchas razones diferentes para iterar sobre una matriz: verifique si existe un valor específico, transforme la matriz en otra matriz, calcule un valor agregado, filtre algunos valores fuera de la matriz ... Si usa un bucle plano simple, no está claro de un vistazo específicamente cómo se está utilizando el bucle for. Sin embargo, la mayoría de los lenguajes modernos tienen API ricas en sus estructuras de datos de matriz que hacen que estos diferentes intentos sean muy explícitos.

Compare transformar una matriz en otra con un bucle for:

int[] doubledArray = new int[array.length];
for (int i = 0; i < array.length; i++) {
  doubledArray[i] = array[i] * 2;
}

y usando una mapfunción de estilo JavaScript :

array.map((value) => value * 2);

O sumando una matriz:

int sum = 0;
for (int i = 0; i < array.length; i++) {
  sum += array[i];
}

versus:

array.reduce(
  (sum, nextValue) => sum + nextValue,
  0
);

¿Cuánto tiempo te lleva entender lo que hace?

int[] newArray = new int[array.length];
int numValuesAdded = 0;

for (int i = 0; i < array.length; i++) {
  if (array[i] >= 0) {
    newArray[numValuesAdded] = array[i];
    numValuesAdded++;
  }
}

versus

array.filter((value) => (value >= 0));

En los tres casos, aunque el bucle for es ciertamente legible, debe dedicar unos minutos para descubrir cómo se está utilizando el bucle for y verificar que todos los contadores y las condiciones de salida sean correctos. Las funciones modernas de estilo lambda hacen que los propósitos de los bucles sean extremadamente explícitos, y usted sabe con certeza que las funciones API que se están llamando se implementan correctamente.

La mayoría de los lenguajes modernos, incluidos JavaScript , Ruby , C # y Java , utilizan este estilo de interacción funcional con matrices y colecciones similares.

En general, si bien no creo que usar for loops sea necesariamente incorrecto, y es una cuestión de gusto personal, me he encontrado muy a favor de usar este estilo de trabajar con matrices. Esto se debe específicamente a la mayor claridad en la determinación de lo que está haciendo cada ciclo. Si su idioma tiene características o herramientas similares en sus bibliotecas estándar, le sugiero que considere adoptar este estilo también.

Kevin
fuente
2
La recomendación array.findplantea la pregunta, ya que luego tenemos que discutir la mejor manera de implementar array.find. A menos que esté utilizando hardware con una findoperación incorporada , tenemos que escribir un bucle allí.
Barmar
2
@Barmar no estoy de acuerdo. Como indiqué en mi respuesta, muchos lenguajes muy utilizados proporcionan funciones como finden sus bibliotecas estándar. Sin lugar a dudas, estas bibliotecas implementan findy sus parientes usan bucles for, pero eso es lo que hace una buena función: abstrae los detalles técnicos del consumidor de la función, permitiendo que el programador no necesite pensar en esos detalles. Por lo tanto, aunque findes probable que se implemente con un bucle for, todavía ayuda a que el código sea más legible, y dado que con frecuencia está en la biblioteca estándar, su uso no agrega una sobrecarga o riesgo significativo.
Kevin
44
Pero un ingeniero de software tiene que implementar estas bibliotecas. ¿No se aplican los mismos principios de ingeniería de software a los autores de bibliotecas que a los programadores de aplicaciones? La pregunta se trata de escribir bucles en general, no es la mejor manera de buscar un elemento de matriz en un idioma en particular
Barmar
44
En otras palabras, buscar un elemento de matriz es solo un ejemplo simple que utilizó para demostrar las diferentes técnicas de bucle.
Barmar
-2

Todo se reduce a precisamente lo que se entiende por "mejor". Para programadores prácticos, generalmente significa eficiente, es decir, en este caso, salir directamente del bucle evita una comparación adicional, y devolver una constante booleana evita una comparación duplicada; Esto ahorra ciclos. Dijkstra está más preocupado por crear código que sea más fácil de probar que es correcto . [Me ha parecido que la educación en CS en Europa toma 'probar la corrección del código' mucho más en serio que la educación en CS en los EE. UU., donde las fuerzas económicas tienden a dominar la práctica de codificación]

PMar
fuente
3
PMar, en cuanto al rendimiento, ambos bucles son prácticamente equivalentes: ambos tienen dos comparaciones.
Danila Piatov
1
Si uno realmente se preocupara por el rendimiento, usaría un algoritmo más rápido. por ejemplo, ordenar la matriz y hacer una búsqueda binaria, o usar una tabla hash.
user949300
Danila: no sabes qué estructura de datos hay detrás de esto. Un iterador siempre es rápido. El acceso indexado puede ser tiempo lineal, y la longitud ni siquiera necesita existir.
gnasher729