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
; for
ne 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 foreach
ressemble:
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é?
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 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:
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 select
requête était précédée d'une requête qui insère également une ligne dans la table. Effectivement, nous aurions une for
base 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 IEnumerable
et 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 ElementAt
mé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’ foreach
alternative. 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 foreach
qu’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 foreach
demandé seulement les villes avant "Boston", le nouveau for
né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 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, cela while
peut ê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.