pour vs. foreach contre LINQ

86

Lorsque j'écris du code dans Visual Studio, ReSharper (que Dieu le bénisse!) Me suggère souvent de changer mon boucle old-school dans le format plus compact foreach.

Et souvent, quand j'accepte ce changement, ReSharper fait un pas en avant et me propose de le changer à nouveau, sous une forme LINQ brillante.

Je me demande donc: ces améliorations présentent-elles de réels avantages? En exécution de code assez simple, je ne vois aucune augmentation de vitesse (évidemment), mais je peux voir le code devenir de moins en moins lisible ... Je me demande donc: est-ce que cela en vaut la peine?

beccoblu
la source
2
Juste une remarque - La syntaxe LINQ est en fait assez lisible si vous êtes familier avec la syntaxe SQL. Il existe également deux formats pour LINQ (les expressions lambda de type SQL et les méthodes chaînées), ce qui peut faciliter l'apprentissage. C’est peut-être simplement les suggestions de ReSharper qui le rendent illisible.
Shauna
3
En règle générale, j'utilise généralement foreach sauf si je travaille avec un tableau de longueurs connues ou dans des cas similaires où le nombre d'itérations est pertinent. Pour ce qui est de LINQ, je vais généralement voir ce que ReSharper fait d'un foreach, et si l'instruction LINQ résultante est ordonnée / triviale / lisible, je l'utilise, sinon je la retourne. Si ce serait une corvée de réécrire la logique originale non-LINQ si les exigences étaient modifiées ou s'il était nécessaire de déboguer de manière granulaire via la logique à partir de laquelle l'instruction LINQ s'abstrait, je ne la laisse pas sous LINQ et la laisser longtemps forme.
Ed Hastings
Une erreur courante foreachconsiste à supprimer des éléments d'une collection tout en les énumérant, où une forboucle est généralement nécessaire pour commencer à partir du dernier élément.
Slai
Vous pouvez tirer profit de Øredev 2013 - Jessica Kerr - Principes fonctionnels pour les développeurs orientés objet . Linq entre dans la présentation peu après la fin des 33 minutes, sous le titre "Style déclaratif".
Theraot le

Réponses:

139

for contre. foreach

Il y a une confusion commune sur le fait que ces deux constructions sont très similaires et que les deux sont interchangeables comme ceci:

foreach (var c in collection)
{
    DoSomething(c);
}

et:

for (var i = 0; i < collection.Count; i++)
{
    DoSomething(collection[i]);
}

Le fait que les deux mots-clés commencent par les trois mêmes lettres ne signifie pas que, sémantiquement, ils sont similaires. Cette confusion est extrêmement sujette aux erreurs, en particulier pour les débutants. Itérer dans une collection et faire quelque chose avec les éléments est fait avec foreach; forne doit pas et ne doit pas être utilisé à cette fin , sauf si vous savez vraiment ce que vous faites.

Voyons ce qui ne va pas avec un exemple. À la fin, vous trouverez le code complet d'une application de démonstration utilisée pour rassembler les résultats.

Dans l'exemple, nous chargeons certaines données de la base de données, plus précisément les villes d'Adventure Works, classées par nom, avant de rencontrer "Boston". La requête SQL suivante est utilisée:

select distinct [City] from [Person].[Address] order by [City]

Les données sont chargées par la ListCities()méthode qui retourne un IEnumerable<string>. Voici à quoi foreachressemble:

foreach (var city in Program.ListCities())
{
    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

Réécrivons-le avec un for, en supposant que les deux sont interchangeables:

var cities = Program.ListCities();
for (var i = 0; i < cities.Count(); i++)
{
    var city = cities.ElementAt(i);

    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

Tous deux retournent les mêmes villes, mais il y a une différence énorme.

  • Lors de l'utilisation foreach, ListCities()est appelé une fois et donne 47 articles.
  • Lors de l'utilisation for, ListCities()est appelé 94 fois et donne 28153 articles en tout.

Qu'est-il arrivé?

IEnumerableest paresseux . Cela signifie qu'il ne fera le travail qu'au moment où le résultat est nécessaire. L'évaluation paresseuse est un concept très utile, mais présente certaines réserves, notamment le fait qu'il est facile de rater le (s) moment (s) où le résultat sera nécessaire, en particulier dans les cas où le résultat est utilisé plusieurs fois.

Dans le cas d'un foreach, le résultat n'est demandé qu'une fois. Dans le cas de a for tel qu’implémenté dans le code écrit de manière incorrecte ci - dessus , le résultat est demandé 94 fois , soit 47 × 2:

  • Chaque fois cities.Count()est appelé (47 fois),

  • Chaque fois cities.ElementAt(i)est appelé (47 fois).

Interroger une base de données 94 fois au lieu d'une est terrible, mais ce n'est pas la pire chose qui puisse arriver. Imaginons, par exemple, ce qui se passerait si la selectrequête était précédée d'une requête qui insère également une ligne dans la table. Effectivement, nous aurions une forbase de données appelée 2,147,483,647 fois dans la base de données , à moins que celle-ci ne tombe en panne auparavant.

Bien sûr, mon code est biaisé. J'ai délibérément utilisé la paresse de IEnumerableet l' ai écrit de manière à appeler à plusieurs reprises ListCities(). On peut noter qu'un débutant ne fera jamais cela, car:

  • Le IEnumerable<T>n'a pas la propriété Count, mais seulement la méthode Count(). L'appel d'une méthode est effrayant, et on peut s'attendre à ce que le résultat ne soit pas mis en cache et ne convienne pas dans un for (; ...; )bloc.

  • L'indexation n'est pas disponible pour IEnumerable<T>et il n'est pas évident de trouver la ElementAtméthode d'extension LINQ.

La plupart des débutants convertiraient probablement le résultat ListCities()en quelque chose qu'ils connaissent bien, comme un List<T>.

var cities = Program.ListCities();
var flushedCities = cities.ToList();
for (var i = 0; i < flushedCities.Count; i++)
{
    var city = flushedCities[i];

    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

Néanmoins, ce code est très différent de l’ foreachalternative. Encore une fois, cela donne les mêmes résultats, et cette fois, la ListCities()méthode n’est appelée qu’une fois, mais produit 575 éléments, alors foreachqu’elle ne donne que 47 éléments.

La différence provient du fait que ToList()provoque toutes les données à charger à partir de la base de données. Bien que foreachdemandé seulement les villes avant "Boston", le nouveau fornécessite que toutes les villes soient récupérées et stockées en mémoire. Avec 575 chaînes courtes, cela ne fait probablement pas beaucoup de différence, mais que se passerait-il si nous ne récupérions que quelques lignes d'une table contenant des milliards d'enregistrements?

Alors qu'est-ce que c'est foreachvraiment?

foreachest plus proche d'une boucle while. Le code que j'ai utilisé précédemment:

foreach (var city in Program.ListCities())
{
    Console.Write(city + " ");

    if (city == "Boston")
    {
        break;
    }
}

peut être simplement remplacé par:

using (var enumerator = Program.ListCities().GetEnumerator())
{
    while (enumerator.MoveNext())
    {
        var city = enumerator.Current;
        Console.Write(city + " ");

        if (city == "Boston")
        {
            break;
        }
    }
}

Les deux produisent le même IL. Les deux ont le même résultat. Les deux ont les mêmes effets secondaires. Bien sûr, cela whilepeut être réécrit dans un infini similaire for, mais ce serait encore plus long et sujet aux erreurs. Vous êtes libre de choisir celui que vous trouvez plus lisible.

Voulez-vous tester vous-même? Voici le code complet:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics;
using System.Linq;

public class Program
{
    private static int countCalls;

    private static int countYieldReturns;

    public static void Main()
    {
        Program.DisplayStatistics("for", Program.UseFor);
        Program.DisplayStatistics("for with list", Program.UseForWithList);
        Program.DisplayStatistics("while", Program.UseWhile);
        Program.DisplayStatistics("foreach", Program.UseForEach);

        Console.WriteLine("Press any key to continue...");
        Console.ReadKey(true);
    }

    private static void DisplayStatistics(string name, Action action)
    {
        Console.WriteLine("--- " + name + " ---");

        Program.countCalls = 0;
        Program.countYieldReturns = 0;

        var measureTime = Stopwatch.StartNew();
        action();
        measureTime.Stop();

        Console.WriteLine();
        Console.WriteLine();
        Console.WriteLine("The data was called {0} time(s) and yielded {1} item(s) in {2} ms.", Program.countCalls, Program.countYieldReturns, measureTime.ElapsedMilliseconds);
        Console.WriteLine();
    }

    private static void UseFor()
    {
        var cities = Program.ListCities();
        for (var i = 0; i < cities.Count(); i++)
        {
            var city = cities.ElementAt(i);

            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseForWithList()
    {
        var cities = Program.ListCities();
        var flushedCities = cities.ToList();
        for (var i = 0; i < flushedCities.Count; i++)
        {
            var city = flushedCities[i];

            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseForEach()
    {
        foreach (var city in Program.ListCities())
        {
            Console.Write(city + " ");

            if (city == "Boston")
            {
                break;
            }
        }
    }

    private static void UseWhile()
    {
        using (var enumerator = Program.ListCities().GetEnumerator())
        {
            while (enumerator.MoveNext())
            {
                var city = enumerator.Current;
                Console.Write(city + " ");

                if (city == "Boston")
                {
                    break;
                }
            }
        }
    }

    private static IEnumerable<string> ListCities()
    {
        Program.countCalls++;
        using (var connection = new SqlConnection("Data Source=mframe;Initial Catalog=AdventureWorks;Integrated Security=True"))
        {
            connection.Open();

            using (var command = new SqlCommand("select distinct [City] from [Person].[Address] order by [City]", connection))
            {
                using (var reader = command.ExecuteReader(CommandBehavior.SingleResult))
                {
                    while (reader.Read())
                    {
                        Program.countYieldReturns++;
                        yield return reader["City"].ToString();
                    }
                }
            }
        }
    }
}

Et les résultats:

--- pour ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston

Les données ont été appelées 94 fois et ont produit 28153 articles.

--- pour avec liste ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston

Les données ont été appelées 1 fois et ont produit 575 objets.

--- pendant ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston

Les données ont été appelées 1 fois et ont produit 47 article (s).

--- --- foreach
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston

Les données ont été appelées 1 fois et ont produit 47 article (s).

LINQ vs. manière traditionnelle

En ce qui concerne LINQ, vous voudrez peut-être apprendre la programmation fonctionnelle (FP) - et non pas avec des choses sur C # FP, mais sur un vrai langage FP comme Haskell. Les langages fonctionnels ont une manière spécifique d'exprimer et de présenter le code. Dans certaines situations, il est supérieur aux paradigmes non fonctionnels.

On sait que la PF est beaucoup plus efficace lorsqu'il s'agit de manipuler des listes ( liste en tant que terme générique, sans rapport avec List<T>). Compte tenu de ce fait, la possibilité d’exprimer du code C # de manière plus fonctionnelle en matière de listes est plutôt une bonne chose.

Si vous n'êtes pas convaincu, comparez la lisibilité du code écrit de manière fonctionnelle et non fonctionnelle dans ma réponse précédente sur le sujet.

Arseni Mourzenko
la source
1
Question sur l'exemple ListCities (). Pourquoi ne fonctionnerait-il qu'une fois? Je n'ai eu aucun problème pour obtenir plus de rendements que par le passé.
Dante
1
Il ne dit pas que vous n'obtiendrez qu'un seul résultat avec IEnumerable - il dit que la requête SQL (qui est la partie coûteuse de la méthode) ne s'exécute qu'une fois - c'est une bonne chose. Il lirait ensuite et générerait tous les résultats de la requête.
HappyCat
9
@Giorgio: Bien que cette question soit compréhensible, avoir la sémantique d'une langue qui laisse penser à ce qu'un débutant pourrait trouver déroutant ne nous laisserait pas avec une langue très efficace.
Steven Evers
4
LINQ n'est pas qu'un sucre sémantique. Il fournit une exécution différée. Et dans le cas de IQueryables (par exemple, Entity Framework), la requête est transmise et composée jusqu'à ce qu'elle soit itérée (ce qui signifie que l'ajout d'une clause where à un IQueryable renvoyé aura pour résultat que le code SQL sera transmis au serveur lors de l'itération afin d'inclure cette clause where décharger le filtrage sur le serveur).
Michael Brown
8
Bien que cette réponse me plaise, je pense que les exemples sont quelque peu artificiels. Le résumé à la fin suggère qu’il foreachest plus efficace que for, alors qu’en réalité, la disparité résulte d’un code délibérément cassé. La minutie de la réponse se rachète, mais il est facile de voir comment un observateur occasionnel pourrait tirer des conclusions erronées.
Robert Harvey
19

Il existe déjà d'excellentes expositions sur les différences entre for et foreach. Il existe certaines fausses représentations du rôle de LINQ.

La syntaxe LINQ n'est pas simplement un sucre syntaxique donnant une approximation fonctionnelle de la programmation à C #. LINQ fournit des constructions fonctionnelles incluant tous les avantages de celles-ci pour C #. Combiné au renvoi de IEnumerable au lieu de IList, LINQ permet l'exécution différée de l'itération. Ce que les gens font généralement maintenant, c’est construire et renvoyer un IListe de leurs fonctions comme

public IList<Foo> GetListOfFoo()
{
   var retVal=new List<Foo>();
   foreach(var foo in _myPrivateFooList)
   {
      if(foo.DistinguishingValue == check)
      {
         retVal.Add(foo);
      }
   }
   return retVal;
}

Utilisez plutôt la syntaxe return return pour créer une énumération différée.

public IEnumerable<Foo> GetEnumerationOfFoo()
{
   //no need to create an extra list
   //var retVal=new List<Foo>();
   foreach(var foo in _myPrivateFooList)
   {
      if(foo.DistinguishingValue == check)
      {
         //yield the match compiler handles the complexity
         yield return foo;
      }
   }
   //no need for returning a list
   //return retVal;
}

Maintenant, l'énumération ne se produira pas tant que vous n'aurez pas à faire la liste des tâches ni à la parcourir. Et cela ne se produit que selon les besoins (voici une énumération de Fibbonaci qui n'a pas de problème de débordement de pile)

/**
Returns an IEnumerable of fibonacci sequence
**/
public IEnumerable<int> Fibonacci()
{
  int first, second = 1;
  yield return first;
  yield return second;
  //the 46th fibonacci number is the largest that
  //can be represented in 32 bits. 
  for (int i = 3; i < 47; i++)
  {
    int retVal = first+second;
    first=second;
    second=retVal;
    yield return retVal;
  }
}

Effectuer un foreach sur la fonction de Fibonacci retournera la séquence de 46. Si vous voulez le 30, c'est tout ce qui sera calculé

var thirtiethFib=Fibonacci().Skip(29).Take(1);

Le support dans le langage des expressions lambda (combiné aux constructions IQueryable et IQueryProvider, permet de composer de manière fonctionnelle des requêtes sur divers ensembles de données. Il incombe à IQueryProvider d’interpréter les données passées. expressions et création et exécution d'une requête à l'aide des constructions natives de la source). Je n'entrerai pas dans les détails sérieux ici, mais il y a une série de billets de blog montrant comment créer un fournisseur de requêtes SQL ici

En résumé, vous devriez préférer renvoyer IEnumerable à IList lorsque les utilisateurs de votre fonction effectueront une simple itération. Et utilisez les fonctionnalités de LINQ pour différer l'exécution de requêtes complexes jusqu'à leur utilisation.

Michael Brown
la source
13

mais je peux voir le code devenir de moins en moins lisible

La lisibilité est dans l'oeil du spectateur. Certaines personnes pourraient dire

var common = list1.Intersect(list2);

est parfaitement lisible; d'autres pourraient dire que c'est opaque, et préféreraient

List<int> common = new List<int>();
for(int i1 = 0; i1 < list1.Count; i1++)
{
    for(int i2 = 0; i2 < list2.Count; i2++)
    {
        if (list1[i1] == list2[i2])
        {
            common.Add(i1);
            break;
        }
    }
}

en clarifiant ce qui se fait. Nous ne pouvons pas vous dire ce que vous trouvez plus lisible. Mais vous pourrez peut-être détecter certains de mes propres biais dans l'exemple que j'ai construit ici ...

AakashM
la source
28
Honnêtement, je dirais que Linq rend l’objectif objectivement plus lisible alors que les boucles for rendent le mécanisme objectivement plus lisible.
jk.
16
Je courrais aussi vite que possible de la part de quelqu'un qui me dit que la version pour-pour-si est plus lisible que la version avec intersection.
Konamiman
3
@ Konamiman - Cela dépend de ce qu'une personne recherche lorsqu'elle pense à la "lisibilité". Le commentaire de jk. illustre parfaitement cela. La boucle est plus lisible en ce sens que vous pouvez facilement voir comment elle obtient son résultat final, tandis que LINQ est plus lisible dans ce que le résultat final devrait être.
Shauna
2
C'est pourquoi la boucle va dans l'implémentation, puis vous utilisez Intersect partout.
R. Martinho Fernandes
8
@Shauna: Imaginez la version de la boucle for dans une méthode faisant plusieurs autres choses; c'est le bordel. Alors, naturellement, vous le divisez en sa propre méthode. En ce qui concerne la lisibilité, il s’agit de la même chose que IEnumerable <T> .Intersect, mais vous avez à présent dupliqué la fonctionnalité d’infrastructure et introduit plus de code à gérer. La seule excuse est si vous avez besoin d'une implémentation personnalisée pour des raisons de comportement, mais nous ne parlons ici que de lisibilité.
Misko
7

La différence entre LINQ et foreachse résume vraiment à deux styles de programmation différents: impératif et déclaratif.

  • Impératif: dans ce style, vous dites à l'ordinateur "fais ceci ... maintenant fais ceci ... maintenant fais ceci maintenant fais ceci". Vous alimentez un programme une étape à la fois.

  • Déclaratif: dans ce style, vous indiquez à l'ordinateur ce que vous voulez que le résultat soit et laissez-le déterminer comment y parvenir.

Un exemple classique de ces deux styles compare le code d'assemblage (ou C) avec SQL. Lors du montage, vous donnez (littéralement) les instructions une par une. En SQL, vous indiquez comment associer des données et quel résultat vous souhaitez obtenir de ces données.

Un effet secondaire intéressant de la programmation déclarative est qu’elle a tendance à être un peu plus avancée. Cela permet à la plate-forme d'évoluer en dessous de vous sans que vous ayez à changer votre code. Par exemple:

var foo = bar.Distinct();

Que se passe-t-il ici? Est-ce que Distinct utilise un seul noyau? Deux? Cinquante? On ne sait pas et on s'en fiche. Les développeurs .NET peuvent le réécrire à tout moment, tant qu’il continue à remplir le même objectif, notre code pourrait s’accélérer comme par magie après une mise à jour du code.

C'est le pouvoir de la programmation fonctionnelle. Et la raison pour laquelle vous trouvez ce code dans des langages comme Clojure, F # et C # (écrit avec un état d'esprit de programmation fonctionnel) est souvent 3x-10x plus petit que ses équivalents impératifs.

Enfin, j'aime bien le style déclaratif car en C #, la plupart du temps, cela me permet d'écrire du code qui ne mute pas les données. Dans l'exemple ci-dessus, Distinct()ne change pas de barre, il retourne une nouvelle copie des données. Cela signifie que quelle que soit la barre, quelle que soit sa provenance, elle n’a pas soudainement changé.

Donc, comme le disent les autres affiches, apprenez la programmation fonctionnelle. Ça va changer ta vie. Et si vous le pouvez, faites-le dans un vrai langage de programmation fonctionnel. Je préfère Clojure, mais F # et Haskell sont également d'excellents choix.

Timothy Baldridge
la source
2
Le traitement LINQ est différé jusqu'à ce que vous le parcouriez. var foo = bar.Distinct()est essentiellement un IEnumerator<T>jusqu'à ce que vous appeliez .ToList()ou .ToArray(). C'est une distinction importante parce que si vous ne le savez pas, cela peut donner lieu à des bogues difficiles à comprendre.
Berin Loritsch
-5

D'autres développeurs de l'équipe peuvent-ils lire LINQ?

Sinon, ne l'utilisez pas ou l'une des deux choses suivantes se produira:

  1. Votre code sera impossible à maintenir
  2. Vous serez obligé de maintenir tout votre code et tout ce qui en dépend

Une boucle pour chaque boucle est parfaite pour parcourir une liste, mais si ce n’est pas ce que vous voulez faire, n’en utilisez pas une.

Lama inversé
la source
11
hmm, j'apprécie que pour un seul projet cela puisse être la solution, mais à moyen ou long terme, vous devez former votre personnel, sinon vous aurez une course au fond de la compréhension du code qui ne semble pas être une bonne idée.
jk.
21
En fait, il y a une troisième chose qui pourrait arriver: les autres développeurs pourraient faire un petit effort et apprendre quelque chose de nouveau et d’utile. Ce n'est pas du jamais vu.
Eric King
6
@ InvertedLlama, si j'étais dans une entreprise où les développeurs ont besoin d'une formation formelle pour comprendre les nouveaux concepts linguistiques, je penserais à trouver une nouvelle entreprise.
Wyatt Barnett
13
Vous pouvez peut-être vous en sortir avec les bibliothèques, mais en ce qui concerne les fonctionnalités de base du langage, cela ne suffit pas. Vous pouvez choisir et choisir des cadres. Mais un bon programmeur .NET doit comprendre chaque caractéristique du langage et de la plate-forme principale (System. *). Et étant donné que vous ne pouvez même pas utiliser EF correctement sans utiliser Linq, je dois dire… de nos jours, si vous êtes un programmeur .NET et que vous ne connaissez pas Linq, vous êtes incompétent.
Timothy Baldridge
7
Cela a déjà assez de votes négatifs, alors je n’ajouterai rien à cela, mais un argument en faveur de collègues ignorants / incompétents n’est jamais valable.
Steven Evers