for
vs. foreach
C'è una confusione comune sul fatto che quei due costrutti sono molto simili e che entrambi sono intercambiabili in questo modo:
foreach (var c in collection)
{
DoSomething(c);
}
e:
for (var i = 0; i < collection.Count; i++)
{
DoSomething(collection[i]);
}
Il fatto che entrambe le parole chiave inizino con le stesse tre lettere non significa che semanticamente siano simili. Questa confusione è estremamente soggetta a errori, soprattutto per i principianti. Scorrere una collezione e fare qualcosa con gli elementi è fatto foreach
; for
non deve e non dovrebbe essere usato per questo scopo , a meno che tu non sappia davvero cosa stai facendo.
Vediamo cosa c'è che non va in questo esempio. Alla fine, troverai il codice completo di un'applicazione demo utilizzata per raccogliere i risultati.
Nell'esempio, stiamo caricando alcuni dati dal database, più precisamente le città di Adventure Works, ordinate per nome, prima di incontrare "Boston". Viene utilizzata la seguente query SQL:
select distinct [City] from [Person].[Address] order by [City]
I dati vengono caricati con il ListCities()
metodo che restituisce un IEnumerable<string>
. Ecco come foreach
appare:
foreach (var city in Program.ListCities())
{
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
Riscriviamolo con a for
, supponendo che entrambi siano intercambiabili:
var cities = Program.ListCities();
for (var i = 0; i < cities.Count(); i++)
{
var city = cities.ElementAt(i);
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
Entrambi restituiscono le stesse città, ma c'è un'enorme differenza.
- Quando viene utilizzato
foreach
, ListCities()
viene chiamato una volta e produce 47 articoli.
- Quando viene utilizzato
for
, ListCities()
viene chiamato 94 volte e produce complessivamente 28153 articoli.
Quello che è successo?
IEnumerable
è pigro . Significa che farà il lavoro solo nel momento in cui è necessario il risultato. La valutazione pigra è un concetto molto utile, ma presenta alcune avvertenze, tra cui il fatto che è facile perdere i momenti in cui il risultato sarà necessario, specialmente nei casi in cui il risultato viene utilizzato più volte.
In caso di a foreach
, il risultato è richiesto una sola volta. Nel caso di un for
come implementato nel codice scritto in modo errato sopra , il risultato viene richiesto 94 volte , ovvero 47 × 2:
Ogni volta che cities.Count()
viene chiamato (47 volte),
Ogni volta che cities.ElementAt(i)
viene chiamato (47 volte).
Interrogare un database 94 volte anziché uno è terribile, ma non è la cosa peggiore che può accadere. Immagina, ad esempio, cosa accadrebbe se la select
query fosse preceduta da una query che inserisce anche una riga nella tabella. Bene, avremmo for
che chiamerà il database 2.147.483.647 volte, a meno che, si spera, non si blocchi prima.
Certo, il mio codice è di parte. Ho deliberatamente usato la pigrizia di IEnumerable
e l'ho scritto in un modo per chiamare più volte ListCities()
. Si può notare che un principiante non lo farà mai, perché:
Il IEnumerable<T>
non ha la proprietà Count
, ma solo il metodo Count()
. Chiamare un metodo è spaventoso e ci si può aspettare che il suo risultato non sia memorizzato nella cache e non adatto in un for (; ...; )
blocco.
L'indicizzazione non è disponibile per IEnumerable<T>
e non è ovvio trovare il ElementAt
metodo di estensione LINQ.
Probabilmente la maggior parte dei principianti dovrebbe semplicemente convertire il risultato ListCities()
in qualcosa con cui hanno familiarità, come 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;
}
}
Tuttavia, questo codice è molto diverso dall'alternativa foreach
. Ancora una volta, dà gli stessi risultati, e questa volta il ListCities()
metodo viene chiamato solo una volta, ma produce 575 articoli, mentre con foreach
, ha prodotto solo 47 articoli.
La differenza deriva dal fatto che ToList()
causa il caricamento di tutti i dati dal database. Mentre sono foreach
richieste solo le città prima di "Boston", la nuova for
richiede che tutte le città siano recuperate e archiviate in memoria. Con 575 stringhe brevi, probabilmente non fa molta differenza, ma cosa succederebbe se recuperassimo solo poche righe da una tabella contenente miliardi di record?
Quindi cos'è foreach
, davvero?
foreach
è più vicino a un ciclo while. Il codice che ho usato in precedenza:
foreach (var city in Program.ListCities())
{
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
può essere semplicemente sostituito da:
using (var enumerator = Program.ListCities().GetEnumerator())
{
while (enumerator.MoveNext())
{
var city = enumerator.Current;
Console.Write(city + " ");
if (city == "Boston")
{
break;
}
}
}
Entrambi producono lo stesso IL. Entrambi hanno lo stesso risultato. Entrambi hanno gli stessi effetti collaterali. Naturalmente, questo while
può essere riscritto in un infinito simile for
, ma sarebbe ancora più lungo e soggetto a errori. Sei libero di scegliere quello che trovi più leggibile.
Vuoi provarlo tu stesso? Ecco il codice completo:
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();
}
}
}
}
}
}
E i risultati:
--- per ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
I dati sono stati chiamati 94 volte e hanno prodotto 28153 articoli.
--- per con elenco ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
I dati sono stati chiamati 1 volta (e) e hanno prodotto 575 articoli.
--- while ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
I dati sono stati chiamati 1 volta (e) e hanno prodotto 47 articoli.
--- foreach ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston
I dati sono stati chiamati 1 volta (e) e hanno prodotto 47 articoli.
LINQ vs. modo tradizionale
Per quanto riguarda LINQ, potresti voler imparare la programmazione funzionale (FP), non cose C # FP, ma un vero linguaggio FP come Haskell. I linguaggi funzionali hanno un modo specifico per esprimere e presentare il codice. In alcune situazioni, è superiore ai paradigmi non funzionali.
FP è noto per essere molto superiore quando si tratta di manipolare elenchi ( elenco come termine generico, non correlato a List<T>
). Dato questo fatto, la capacità di esprimere il codice C # in un modo più funzionale quando si tratta di elenchi è piuttosto una buona cosa.
Se non sei convinto, confronta la leggibilità del codice scritto sia in modo funzionale che non funzionale nella mia precedente risposta sull'argomento.