Lorsque j'écris du code dans Visual Studio, ReSharper (que Dieu le bénisse!) Me suggère souvent de changer ma boucle for old-school sous la forme foreach plus compacte.
Et souvent, lorsque j'accepte ce changement, ReSharper fait un pas en avant et me propose de le changer à nouveau, sous une forme LINQ brillante.
Donc, je me demande: y a-t-il des avantages réels, dans ces améliorations? Dans l'exécution de code assez simple, je ne vois aucun boost de vitesse (évidemment), mais je peux voir le code devenir de moins en moins lisible ... Alors je me demande: est-ce que ça vaut le coup?
for
vs foreach
Il y a une confusion commune 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 se fait avec foreach
; for
n'a 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 recueillir 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 méthode ListCities()
qui retourne un IEnumerable<string>
. Voici à quoi ressemble foreach
:
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;
}
}
Les deux retournent les mêmes villes, mais il y a une énorme différence.
foreach
, ListCities()
est appelée une seule fois et produit 47 éléments.for
, ListCities()
est appelée 94 fois et donne 28153 éléments au total.Qu'est-il arrivé?
IEnumerable
est 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 comporte quelques mises en garde, notamment le fait qu'il est facile de manquer le ou les moments 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 seule fois. Dans le cas d'un for
tel qu'implémenté dans le code mal écrit ci-dessus, le résultat est demandé 94 fois , soit 47 × 2:
Chaque fois que cities.Count()
est appelée (47 fois),
Chaque fois que cities.ElementAt(i)
est appelée (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. Imaginez, par exemple, ce qui se passerait si la requête select
était précédée d'une requête qui insère également une ligne dans la table. À droite, nous aurions for
qui appellera la base de données 2 147 483 647 fois, à moins qu'elle ne plante, espérons-le.
Bien sûr, mon code est biaisé. J'ai délibérément utilisé la paresse de IEnumerable
et l'ai écrite de manière à appeler à plusieurs reprises ListCities()
. On peut noter qu'un débutant ne fera jamais ça, car:
Le IEnumerable<T>
N'a pas la propriété Count
, mais seulement la méthode Count()
. Appeler une méthode est effrayant, et on peut s'attendre à ce que son résultat ne soit pas mis en cache et ne convienne pas dans un bloc for (; ...; )
.
L'indexation n'est pas disponible pour IEnumerable<T>
Et il n'est pas évident de trouver la méthode d'extension ElementAt
LINQ.
La plupart des débutants convertiraient probablement le résultat de ListCities()
en quelque chose qu'ils connaissent, 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;
}
}
Pourtant, ce code est très différent de l'alternative foreach
. Encore une fois, cela donne les mêmes résultats, et cette fois la méthode ListCities()
n'est appelée qu'une seule fois, mais donne 575 éléments, tandis qu'avec foreach
, elle a donné seulement 47 articles.
La différence vient du fait que ToList()
provoque toutes les données à charger à partir de la base de données. Alors que foreach
ne demandait que les villes avant "Boston", le nouveau for
exige 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 passe-t-il si nous ne récupérons que quelques lignes d'une table contenant des milliards d'enregistrements?
foreach
, vraiment?foreach
est 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, ce while
peut être réécrit dans un for
infini similaire, mais il serait encore plus long et sujet aux erreurs. Vous êtes libre de choisir celui que vous trouvez le plus lisible.
Vous voulez le 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 BostonLes données ont été appelées 94 fois et ont donné 28153 article (s).
--- pour avec liste ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux BostonLes données ont été appelées 1 fois et ont produit 575 article (s).
--- tandis que ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux BostonLes données ont été appelées 1 fois et ont donné 47 élément (s).
--- pour chaque ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux BostonLes données ont été appelées 1 fois et ont donné 47 élément (s).
Comme pour LINQ, vous voudrez peut-être apprendre la programmation fonctionnelle (FP) - pas C # FP stuff, mais real = FP langage comme Haskell. Les langages fonctionnels ont une façon spécifique d'exprimer et de présenter le code. Dans certaines situations, il est supérieur aux paradigmes non fonctionnels.
FP est connu pour être bien supérieur lorsqu'il s'agit de manipuler des listes ( list 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 ce qui concerne les listes est plutôt une bonne chose.
Si vous n'êtes pas convaincu, comparez la lisibilité du code écrit à la fois de manière fonctionnelle et non fonctionnelle dans mon réponse précédente sur le sujet.
Bien qu'il existe déjà de grandes expositions sur les différences entre for et foreach. Il y a quelques fausses déclarations grossières sur le rôle de LINQ.
La syntaxe LINQ n'est pas seulement du sucre syntaxique donnant une approximation de programmation fonctionnelle à C #. LINQ fournit des constructions fonctionnelles, y compris tous ses avantages pour C #. Combiné avec le retour de IEnumerable au lieu d'IList, LINQ fournit une exécution différée de l'itération. Ce que les gens font généralement maintenant, c'est construire et renvoyer un IList de leurs fonctions comme ça
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 de retour de rendement 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 que lorsque vous ToList ou itérerez dessus. Et cela ne se produit que si nécessaire (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 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);
Lorsque nous obtenons beaucoup de plaisir, c'est le support dans le langage des expressions lambda (combiné avec les constructions IQueryable et IQueryProvider, cela permet une composition fonctionnelle des requêtes par rapport à une variété d'ensembles de données, IQueryProvider est responsable de l'interprétation des données transmises). expressions et créer et exécuter une requête à l'aide des constructions natives de la source). Je n'entrerai pas dans les moindres détails ici, mais il y a une série de billets de blog montrant comment créer un SQL Query Provider ici
En résumé, vous devriez préférer retourner IEnumerable à IList lorsque les consommateurs de votre fonction effectueront une simple itération. Et utilisez les capacités de LINQ pour différer l'exécution de requêtes complexes jusqu'à ce qu'elles soient nécessaires.
mais je peux voir le code devenir de moins en moins lisible
La lisibilité est dans l'œil du spectateur. Certaines personnes pourraient dire
var common = list1.Intersect(list2);
est parfaitement lisible; d'autres pourraient dire que cela 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;
}
}
}
comme rendant plus clair ce qui est fait. Nous ne pouvons pas vous dire ce que vous trouvez plus lisible. Mais vous pourriez être en mesure de détecter certains de mes propres biais dans l'exemple que j'ai construit ici ...
La différence entre LINQ et foreach
se résume vraiment à deux styles de programmation différents: impératif et déclaratif.
Impératif: dans ce style, vous dites à l'ordinateur "faites ceci ... maintenant faites ceci ... maintenant faites ceci maintenant faites ceci". Vous l'alimentez un programme une étape à la fois.
Déclaratif: dans ce style, vous dites à l'ordinateur ce que vous voulez que le résultat soit et laissez-le trouver comment y arriver.
Un exemple classique de ces deux styles compare le code Assembly (ou C) avec SQL. Dans Assembly, vous donnez des instructions (littéralement) une à la fois. En SQL, vous exprimez comment joindre des données ensemble et quel résultat vous voulez de ces données.
Un bon effet secondaire de la programmation déclarative est qu'elle a tendance à être un peu plus élevée. Cela permet à la plateforme d'évoluer sous vous sans que vous ayez à changer votre code. Par exemple:
var foo = bar.Distinct();
Que se passe-t-il ici? Distinct utilise-t-il un noyau? Deux? Cinquante? Nous ne savons pas et nous ne nous soucions pas. Les développeurs .NET pourraient le réécrire à tout moment, tant qu'il continue à remplir le même objectif, notre code pourrait simplement être plus rapide comme par magie après une mise à jour de code.
C'est la puissance de la programmation fonctionnelle. Et la raison pour laquelle vous trouverez que le code dans des langages comme Clojure, F # et C # (écrit avec un état d'esprit de programmation fonctionnelle) est souvent 3x-10x plus petit que ses homologues impératifs.
Enfin j'aime 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 renvoie une nouvelle copie des données. Cela signifie que quelle que soit la barre, et d'où qu'elle vienne, elle n'a pas soudainement changé.
Donc, comme le disent les autres affiches, apprenez la programmation fonctionnelle. Ça va changer votre vie. Et si vous le pouvez, faites-le dans un véritable langage de programmation fonctionnel. Je préfère Clojure, mais F # et Haskell sont également d'excellents choix.