per vs. foreach vs. LINQ


86

Quando scrivo codice in Visual Studio, ReSharper (che Dio lo benedica!) Mi suggerisce spesso di cambiare la mia vecchia scuola per il ciclo nella forma foreach più compatta.

E spesso, quando accetto questo cambiamento, ReSharper fa un passo avanti e mi suggerisce di cambiarlo di nuovo, in un brillante modulo LINQ.

Quindi, mi chiedo: ci sono alcuni vantaggi reali , in questi miglioramenti? Nell'esecuzione abbastanza semplice del codice, non riesco a vedere alcun aumento di velocità (ovviamente), ma posso vedere il codice diventare sempre meno leggibile ... Quindi mi chiedo: ne vale la pena?


2
Solo una nota: la sintassi LINQ è in realtà piuttosto leggibile se hai familiarità con la sintassi SQL. Esistono anche due formati per LINQ (le espressioni lambda simili a SQL e i metodi concatenati), che potrebbero semplificare l'apprendimento. Potrebbero essere solo i suggerimenti di ReSharper a renderlo illeggibile.
Shauna,

3
Come regola generale, in genere utilizzo foreach a meno che non lavori con una matrice di lunghezza nota o casi simili in cui il numero di iterazioni è rilevante. Per quanto riguarda LINQ-ifying, di solito vedrò cosa fa ReSharper di un foreach, e se l'istruzione LINQ risultante è ordinata / banale / leggibile la uso, e altrimenti la ripristino. Se sarebbe una seccatura riscrivere la logica originale non LINQ se i requisiti cambiassero o se potrebbe essere necessario eseguire il debug granulare attraverso la logica da cui l'istruzione LINQ si sta sottraggendo, non la LINQ e la lascio a lungo modulo.
Ed Hastings,

un errore comune foreachè la rimozione di elementi da una raccolta durante l'enumerazione, dove di solito forè necessario un ciclo per iniziare dall'ultimo elemento.
Slai,

Potresti trarre valore da Øredev 2013 - Jessica Kerr - Principi funzionali per gli sviluppatori orientati agli oggetti . Linq entra nella presentazione subito dopo il segno dei 33 minuti, sotto il titolo "Stile dichiarativo".
Theraot,

Risposte:


139

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; fornon 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 foreachappare:

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 selectquery fosse preceduta da una query che inserisce anche una riga nella tabella. Bene, avremmo forche 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 IEnumerablee 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 ElementAtmetodo 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 foreachrichieste solo le città prima di "Boston", la nuova forrichiede 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 whilepuò 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.


1
Domanda sull'esempio di ListCities (). Perché dovrebbe funzionare solo una volta? In passato non ho avuto problemi a superare i rendimenti.
Dante,

1
Non sta dicendo che otterresti un solo risultato da IEnumerable - sta dicendo che la query SQL (che è la parte costosa del metodo) verrà eseguita solo una volta - questa è una buona cosa. Quindi legge e produce tutti i risultati della query.
HappyCat,

9
@Giorgio: Mentre questa domanda è comprensibile, avere la semantica di una lingua che asseconda ciò che un principiante potrebbe trovare confuso non ci lascerebbe con un linguaggio molto efficace.
Steven Evers,

4
LINQ non è solo zucchero semantico. Fornisce l'esecuzione ritardata. E nel caso di IQueryables (ad esempio Entity Framework) consente il passaggio e la composizione della query fino a quando non viene ripetuta (il che significa che l'aggiunta di una clausola where a una IQueryable restituita comporterà il passaggio dell'SQL al server al momento dell'iterazione per includere quella clausola where scaricando il filtro sul server).
Michael Brown,

8
Per quanto mi piaccia questa risposta, penso che gli esempi siano in qualche modo inventati. Il riassunto alla fine suggerisce che foreachè più efficiente di for, quando in realtà la disparità è il risultato di un codice deliberatamente rotto. L'accuratezza della risposta si riscatta da sola, ma è facile vedere come un osservatore occasionale potrebbe giungere a conclusioni errate.
Robert Harvey,

19

Mentre ci sono già alcune grandi esposizioni sulle differenze tra for e foreach. Vi sono alcune grossolane dichiarazioni sul ruolo di LINQ.

La sintassi LINQ non è solo zucchero sintattico che fornisce un'approssimazione di programmazione funzionale a C #. LINQ fornisce costrutti funzionali compresi tutti i vantaggi di C #. In combinazione con la restituzione di IEnumerable anziché IList, LINQ fornisce l'esecuzione differita dell'iterazione. Ciò che le persone fanno normalmente ora è costruire e restituire un IList dalle loro funzioni in questo modo

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

Utilizzare invece la sintassi return return per creare un'enumerazione posticipata.

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;
}

Ora l'enumerazione non si verificherà fino a quando non si ToList o itererà su di esso. E si verifica solo se necessario (ecco un elenco di Fibbonaci che non ha un problema di overflow dello stack)

/**
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;
  }
}

L'esecuzione di una foreach sulla funzione di Fibonacci restituirà la sequenza di 46. Se si desidera il 30 ° è tutto ciò che verrà calcolato

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

Dove ci divertiamo molto è il supporto nel linguaggio per le espressioni lambda (combinato con i costrutti IQueryable e IQueryProvider, questo consente la composizione funzionale delle query su una varietà di set di dati, IQueryProvider è responsabile dell'interpretazione del passaggio in espressioni e creazione ed esecuzione di una query utilizzando i costrutti nativi della sorgente). Non entrerò nei dettagli grintosi qui, ma ci sono una serie di post sul blog che mostrano come creare un provider di query SQL qui

In breve, è preferibile restituire IEnumerable su IList quando gli utenti della funzione eseguiranno una semplice iterazione. E utilizzare le capacità di LINQ per rinviare l'esecuzione di query complesse fino a quando non sono necessarie.


13

ma vedo il codice diventare sempre meno leggibile

La leggibilità è negli occhi di chi guarda. Alcune persone potrebbero dire

var common = list1.Intersect(list2);

è perfettamente leggibile; altri potrebbero dire che questo è opaco e preferirebbero

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;
        }
    }
}

come rendere più chiaro ciò che viene fatto. Non possiamo dirti cosa trovi più leggibile. Ma potresti essere in grado di rilevare alcuni dei miei pregiudizi nell'esempio che ho costruito qui ...


28
Onestamente direi che Linq rende l'intenzione oggettivamente più leggibile mentre per i loop rende il meccanismo oggettivamente più leggibile.
jk.

16
Vorrei correre il più velocemente possibile da qualcuno che mi dice che la versione for-for-if è più leggibile della versione intersect.
Konamiman,

3
@Konamiman - Dipenderebbe da ciò che una persona cerca quando pensa alla "leggibilità". Il commento di jk. lo illustra perfettamente. Il loop è più leggibile, nel senso che puoi facilmente vedere come sta ottenendo il risultato finale, mentre LINQ è più leggibile in quello che dovrebbe essere il risultato finale.
Shauna,

2
Ecco perché il ciclo entra nell'implementazione e quindi usi Intersect ovunque.
R. Martinho Fernandes,

8
@Shauna: immagina la versione for-loop all'interno di un metodo che fa molte altre cose; è un casino. Quindi, naturalmente, lo dividi nel suo metodo. Per quanto riguarda la leggibilità, questo è uguale a IEnumerable <T> .Intersect, ma ora hai duplicato la funzionalità del framework e introdotto più codice da mantenere. L'unica scusa è se hai bisogno di un'implementazione personalizzata per motivi comportamentali, ma stiamo parlando solo di leggibilità qui.
Misko,

7

La differenza tra LINQ e foreachdavvero si riduce a due diversi stili di programmazione: imperativo e dichiarativo.

  • Imperativo: in questo stile dici al computer "fai questo ... ora fai questo ... ora fai questo ora fai questo". Gli dai un programma uno alla volta.

  • Dichiarativo: in questo stile dici al computer quale vuoi che sia il risultato e fai capire come arrivarci.

Un classico esempio di questi due stili è il confronto del codice assembly (o C) con SQL. In assemblea date istruzioni (letteralmente) una alla volta. In SQL si spiega come unire i dati insieme e quale risultato si desidera da tali dati.

Un buon effetto collaterale della programmazione dichiarativa è che tende ad essere un po 'più alto livello. Ciò consente alla piattaforma di evolversi sotto di te senza che tu debba cambiare il tuo codice. Per esempio:

var foo = bar.Distinct();

Cosa sta succedendo qui? Distinct sta usando un core? Due? Cinquanta? Non lo sappiamo e non ci interessa. Gli sviluppatori .NET potrebbero riscriverlo in qualsiasi momento, purché continui a svolgere lo stesso scopo il nostro codice potrebbe magicamente diventare più veloce dopo un aggiornamento del codice.

Questo è il potere della programmazione funzionale. E la ragione per cui troverai quel codice in linguaggi come Clojure, F # e C # (scritto con una mentalità di programmazione funzionale) è spesso 3x-10x più piccola delle sue controparti imperative.

Finalmente mi piace lo stile dichiarativo perché in C # il più delle volte questo mi permette di scrivere codice che non muta i dati. Nell'esempio sopra, Distinct()non cambia barra, restituisce una nuova copia dei dati. Ciò significa che qualunque sia la barra e da dove provenga, non è cambiato improvvisamente.

Quindi, come dicono gli altri poster, impara la programmazione funzionale. Ti cambierà la vita. E se puoi, fallo in un vero linguaggio di programmazione funzionale. Preferisco Clojure, ma anche F # e Haskell sono scelte eccellenti.


2
L'elaborazione LINQ viene rinviata fino a quando non si esegue l'iterazione su di essa. var foo = bar.Distinct()è essenzialmente un IEnumerator<T>finché non chiami .ToList()o .ToArray(). Questa è una distinzione importante perché se non sei consapevole di ciò può portare a bug difficili da capire.
Berin Loritsch,

-5

Gli altri sviluppatori del team possono leggere LINQ?

In caso contrario, non utilizzarlo o accadrà una delle due cose:

  1. Il tuo codice non sarà mantenibile
  2. Sarai bloccato nel mantenere tutto il tuo codice e tutto ciò che si basa su di esso

A per ogni ciclo è perfetto per scorrere un elenco, ma se non è quello che devi fare, non usarne uno.


11
hmm, apprezzo che per un singolo progetto questa potrebbe essere la risposta, ma per il medio-lungo termine dovresti formare il tuo personale, altrimenti hai una corsa al fondo della comprensione del codice che non suona come una buona idea.
jk.

21
In realtà, potrebbe succedere una terza cosa: gli altri sviluppatori potrebbero fare un piccolo sforzo e imparare qualcosa di nuovo e utile. Non è inaudito.
Eric King,

6
@InvertedLlama se fossi in una società in cui gli sviluppatori hanno bisogno di una formazione formale per comprendere nuovi concetti linguistici, penserei di trovare una nuova società.
Wyatt Barnett,

13
Forse puoi liberarti di questo atteggiamento con le biblioteche, ma quando si tratta di funzionalità del linguaggio di base, ciò non lo taglia. È possibile selezionare i framework. Ma un buon programmatore .NET deve comprendere ogni singola caratteristica del linguaggio e della piattaforma principale (Sistema. *). E considerando che non puoi nemmeno usare EF correttamente senza usare Linq, devo dire ... al giorno d'oggi, se sei un programmatore .NET e non conosci Linq, sei incompetente.
Timothy Baldridge,

7
Questo ha già abbastanza voti negativi, quindi non lo aggiungerò, ma un argomento a sostegno di colleghi ignoranti / incompetenti non è mai valido.
Steven Evers,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.