Filtrar bucles foreach con una condición where vs continuar cláusulas de guardia

24

He visto a algunos programadores usar esto:

foreach (var item in items)
{
    if (item.Field != null)
        continue;

    if (item.State != ItemStates.Deleted)
        continue;

    // code
}

en lugar de donde normalmente usaría:

foreach (var item in items.Where(i => i.Field != null && i.State != ItemStates.Deleted))
{
    // code
}

Incluso he visto una combinación de ambos. Realmente me gusta la legibilidad con 'continuar', especialmente con condiciones más complejas. ¿Hay incluso una diferencia en el rendimiento? Con una consulta de base de datos, supongo que la habría. ¿Qué pasa con las listas regulares?

Pimentón
fuente
3
Para listas regulares suena como micro optimización.
apocalipsis
2
@zgnilec: ... pero, ¿cuál de las dos variantes es la versión optimizada? Tengo una opinión al respecto, por supuesto, pero con solo mirar el código, esto no es inherentemente claro para todos.
Doc Brown
2
Por supuesto, continuar será más rápido. Usando linq. Donde creas iterador adicional.
apocalipsis
1
@zgnilec - Buena teoría. ¿ Quieres publicar una respuesta explicando por qué piensas eso? Ambas respuestas que existen actualmente dicen lo contrario.
Bobson
2
... así que la conclusión es: las diferencias de rendimiento entre las dos construcciones son despreciables, y tanto la legibilidad como la depuración se pueden lograr para ambos. Es simplemente una cuestión de gustos cuál prefieres.
Doc Brown

Respuestas:

64

Consideraría esto como un lugar apropiado para usar la separación de comando / consulta . Por ejemplo:

// query
var validItems = items.Where(i => i.Field != null && i.State != ItemStates.Deleted);
// command
foreach (var item in validItems) {
    // do stuff
}

Esto también le permite dar un buen nombre autodocumentado al resultado de la consulta. También le ayuda a ver oportunidades para refactorizar, porque es mucho más fácil refactorizar el código que solo consulta datos o solo muta datos que el código mixto que intenta hacer ambas cosas.

Al depurar, puede romper antes foreachpara verificar rápidamente si el contenido de la validItemsresolución se ajusta a lo que espera. No tiene que pisar el lambda a menos que lo necesite. Si necesita ingresar a la lambda, le sugiero que la descomponga en una función separada, luego en lugar de eso.

¿Hay alguna diferencia en el rendimiento? Si la consulta está respaldada por una base de datos, entonces la versión LINQ tiene el potencial de ejecutarse más rápido, porque la consulta SQL puede ser más eficiente. Si es LINQ to Objects, entonces no verá ninguna diferencia de rendimiento real. Como siempre, perfile su código y corrija los cuellos de botella que realmente se informan, en lugar de tratar de predecir optimizaciones por adelantado.

Christian Hayter
fuente
1
¿Por qué un conjunto de datos extremadamente grande marcaría la diferencia? ¿Solo porque el costo minúsculo de las lambdas eventualmente sumaría?
BlueRaja - Danny Pflughoeft
1
@ BlueRaja-DannyPflughoeft: Sí, tiene razón, este ejemplo no implica una complejidad algorítmica adicional más allá del código original. He eliminado la frase
Christian Hayter
¿No resulta esto en dos iteraciones sobre la colección? Naturalmente, el segundo es más corto, considerando que solo hay elementos válidos dentro de él, pero aún debe hacerlo dos veces, una para filtrar los elementos, la segunda vez para trabajar con los elementos válidos.
Andy
1
@DavidPacker No. El bucle solo lo IEnumerablecontrola foreach.
Benjamin Hodgson
2
@DavidPacker: Eso es exactamente lo que hace; La mayoría de los métodos de LINQ to Objects se implementan utilizando bloques iteradores. El código de ejemplo anterior iterará a través de la colección exactamente una vez, ejecutando el Wherelambda y el cuerpo del bucle (si el lambda devuelve verdadero) una vez por elemento.
Christian Hayter
7

Por supuesto, hay una diferencia en el rendimiento, lo que .Where()resulta en una llamada de delegado para cada elemento. Sin embargo, no me preocuparía en absoluto por el rendimiento:

  • Los ciclos de reloj utilizados para invocar a un delegado son insignificantes en comparación con los ciclos de reloj utilizados por el resto del código que itera sobre la colección y verifica las condiciones.

  • La penalización de rendimiento de invocar a un delegado es del orden de unos pocos ciclos de reloj y, afortunadamente, ya pasamos los días en que teníamos que preocuparnos por los ciclos de reloj individuales.

Si por alguna razón el rendimiento es realmente importante para usted en el nivel del ciclo del reloj, entonces use en List<Item>lugar de IList<Item>, para que el compilador pueda hacer uso de llamadas directas (e inlinables) en lugar de llamadas virtuales, y para que el iterador de List<T>, que en realidad es a struct, no tiene que estar en caja. Pero eso es realmente cosas insignificantes.

Una consulta de base de datos es una situación diferente, porque existe (al menos en teoría) la posibilidad de enviar el filtro al RDBMS, mejorando así enormemente el rendimiento: solo las filas coincidentes harán el viaje del RDBMS a su programa. Pero para eso creo que tendrías que usar linq, no creo que esta expresión se pueda enviar al RDBMS tal como está.

Que realmente va a ver los beneficios de if(x) continue;este momento hay que depurar este código: Single-pasando por encima de if()s y continues funciona muy bien; entrar en el delegado de filtrado es un dolor.

Mike Nakis
fuente
Es entonces cuando algo está mal y desea ver todos los elementos y verificar en el depurador cuáles tienen Field! = Null y cuáles tienen State! = Null; Esto podría ser difícil o imposible con foreach ... dónde.
gnasher729
Buen punto con la depuración. Entrar en un lugar no es tan malo en Visual Studio, pero no puede volver a escribir expresiones lambda durante la depuración sin volver a compilar, y esto lo evita al usarlo if(x) continue;.
Paprik
Estrictamente hablando, .Wheresolo se invoca una vez. Lo que se invoca en cada iteración es el delegado del filtro (y MoveNext, y Currenten el empadronador, cuando ellos no se han optimizado a cabo)
CodesInChaos
@CodesInChaos me tomó un poco de tiempo entender de qué estás hablando, pero por supuesto, wh00ps, tienes razón, estrictamente hablando, .Wheresolo se invoca una vez. Arreglado.
Mike Nakis