Filtrage des boucles foreach avec une condition where vs clause de garde continue

24

J'ai vu certains programmeurs utiliser ceci:

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

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

    // code
}

au lieu de celui que j'utiliserais normalement:

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

J'ai même vu une combinaison des deux. J'aime vraiment la lisibilité avec «continuer», en particulier avec des conditions plus complexes. Y a-t-il même une différence de performances? Avec une requête de base de données, je suppose qu'il y en aurait. Et les listes régulières?

Paprik
la source
3
Pour les listes régulières, cela ressemble à une micro-optimisation.
apocalypse du
2
@zgnilec: ... mais en réalité laquelle des deux variantes est la version optimisée? J'ai une opinion là-dessus, bien sûr, mais à la simple lecture du code, ce n'est pas intrinsèquement clair pour tout le monde.
Doc Brown
2
Bien sûr, continuer sera plus rapide. Utilisation de linq. Où vous créez un itérateur supplémentaire.
apocalypse
1
@zgnilec - Bonne théorie. Prenez soin de poster une réponse expliquant pourquoi vous pensez cela? Les deux réponses qui existent actuellement disent le contraire.
Bobson
2
... le résultat est le suivant: les différences de performances entre les deux constructions sont négligeables, et la lisibilité ainsi que le débogage peuvent être atteints pour les deux. C'est simplement une question de goût que vous préférez.
Doc Brown

Réponses:

64

Je considérerais cela comme un endroit approprié pour utiliser la séparation commande / requête . Par exemple:

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

Cela vous permet également de donner un bon nom auto-documenté au résultat de la requête. Il vous aide également à voir les opportunités de refactoring, car il est beaucoup plus facile de refactoriser du code qui interroge uniquement des données ou ne mute que des données que du code mixte qui essaie de faire les deux.

Lors du débogage, vous pouvez interrompre avant foreachde vérifier rapidement si le contenu de la validItemsrésolution correspond à ce que vous attendez. Vous n'avez pas à entrer dans le lambda, sauf si vous en avez besoin. Si vous devez entrer dans le lambda, je suggère de le factoriser dans une fonction distincte, puis de le parcourir à la place.

Y a-t-il une différence de performances? Si la requête est soutenue par une base de données, la version LINQ peut s'exécuter plus rapidement, car la requête SQL peut être plus efficace. S'il s'agit de LINQ to Objects, vous ne verrez aucune différence réelle de performances. Comme toujours, profilez votre code et corrigez les goulots d'étranglement qui sont réellement signalés, plutôt que d'essayer de prévoir les optimisations à l'avance.

Christian Hayter
la source
1
Pourquoi un ensemble de données extrêmement volumineux ferait-il une différence? Tout simplement parce que le coût minuscule des lambdas finirait par s'additionner?
BlueRaja - Danny Pflughoeft
1
@ BlueRaja-DannyPflughoeft: Oui, vous avez raison, cet exemple n'implique aucune complexité algorithmique supplémentaire au-delà du code d'origine. J'ai supprimé la phrase.
Christian Hayter du
Cela ne se traduit-il pas par deux itérations sur la collection? Naturellement, le second est plus court, car seuls les éléments valides s'y trouvent, mais vous devez toujours le faire deux fois, une fois pour filtrer les éléments, la deuxième fois pour travailler avec les éléments valides.
Andy
1
@DavidPacker No. Le IEnumerableest foreachuniquement piloté par la boucle.
Benjamin Hodgson
2
@DavidPacker: C'est exactement ce qu'il fait; la plupart des méthodes LINQ to Objects sont implémentées à l'aide de blocs d'itérateur. L'exemple de code ci-dessus parcourra la collection exactement une fois, exécutant le Wherelambda et le corps de la boucle (si le lambda renvoie vrai) une fois par élément.
Christian Hayter
7

Bien sûr, il existe une différence de performances, ce qui .Where()entraîne un appel délégué pour chaque élément. Cependant, je ne me soucierais pas du tout des performances:

  • Les cycles d'horloge utilisés pour appeler un délégué sont négligeables par rapport aux cycles d'horloge utilisés par le reste du code qui itère sur la collection et vérifie les conditions.

  • La pénalité de performance d'invoquer un délégué est de l'ordre de quelques cycles d'horloge, et heureusement, nous avons longtemps dépassé les jours où nous devions nous soucier des cycles d'horloge individuels.

Si pour une raison quelconque, les performances sont vraiment importantes pour vous au niveau du cycle d'horloge, utilisez List<Item>plutôt à la place de IList<Item>, afin que le compilateur puisse utiliser des appels directs (et inlinables) au lieu d'appels virtuels, et pour que l'itérateur de List<T>, qui est en fait a struct, ne doit pas être encadré. Mais c'est vraiment des trucs insignifiants.

Une requête de base de données est une situation différente, car il y a (au moins en théorie) une possibilité d'envoyer le filtre au SGBDR, améliorant ainsi considérablement les performances: seules les lignes correspondantes feront le trajet du SGBDR vers votre programme. Mais pour cela, je pense que vous devriez utiliser linq, je ne pense pas que cette expression puisse être envoyée au SGBDR tel quel.

Vous verrez vraiment les avantages du if(x) continue;moment où vous devez déboguer ce code: le passage simple sur if()s et continues fonctionne bien; une seule étape dans le délégué de filtrage est une douleur.

Mike Nakis
la source
C'est quand quelque chose ne va pas et que vous voulez regarder tous les éléments et vérifier dans le débogueur ceux qui ont Field! = Null et ceux qui ont State! = Null; cela pourrait être difficile voire impossible avec foreach ... où.
gnasher729
Bon point avec le débogage. Entrer dans un où n'est pas si mal dans Visual Studio, mais vous ne pouvez pas réécrire les expressions lambda pendant le débogage sans recompiler, ce que vous évitez lors de l'utilisation if(x) continue;.
Paprik
Strictement parlant, .Wheren'est invoqué qu'une seule fois. Ce qui est invoqué à chaque itération est le délégué du filtre (et MoveNextet Currentsur le recenseur, quand ils ne sont pas optimisés out)
CodesInChaos
@CodesInChaos il m'a fallu un peu de réflexion pour comprendre de quoi vous parlez, mais bien sûr, wh00ps, vous avez raison, à proprement parler, .Wheren'est invoqué qu'une seule fois. A corrigé.
Mike Nakis