untuk vs foreach vs LINQ

86

Ketika saya menulis kode dalam Visual Studio, ReSharper (Tuhan memberkati itu!) Sering menyarankan saya untuk mengubah sekolah lama saya untuk loop dalam bentuk foreach yang lebih kompak.

Dan sering, ketika saya menerima perubahan ini, ReSharper melangkah maju, dan menyarankan saya untuk mengubahnya lagi, dalam bentuk LINQ yang mengkilap.

Jadi, saya bertanya-tanya: apakah ada beberapa keuntungan nyata , dalam perbaikan ini? Dalam eksekusi kode yang cukup sederhana, saya tidak dapat melihat peningkatan kecepatan (jelas), tetapi saya dapat melihat kode menjadi semakin kurang dapat dibaca ... Jadi saya bertanya-tanya: apakah itu layak?

beccoblu
sumber
2
Sekedar catatan - sintaks LINQ sebenarnya cukup mudah dibaca jika Anda terbiasa dengan sintaks SQL. Ada juga dua format untuk LINQ (ekspresi lambda seperti SQL, dan metode dirantai), yang mungkin membuatnya lebih mudah untuk dipelajari. Mungkin saja saran ReSharper yang membuatnya tampak tidak dapat dibaca.
Shauna
3
Sebagai aturan praktis, saya biasanya menggunakan foreach kecuali bekerja dengan array panjang yang diketahui atau kasus serupa di mana jumlah iterasi relevan. Adapun LINQ-ifying itu, saya biasanya akan melihat apa ReSharper membuat foreach, dan jika pernyataan LINQ yang dihasilkan rapi / sepele / mudah dibaca saya menggunakannya, dan kalau tidak saya mengembalikannya kembali. Jika itu akan menjadi tugas untuk menulis kembali logika non-LINQ asli jika persyaratan berubah atau jika mungkin perlu untuk debug secara terperinci melalui logika pernyataan LINQ yang abstrak, saya tidak LINQ dan meninggalkannya dalam waktu lama bentuk, kondisi.
Ed Hastings
satu kesalahan umum foreachadalah menghapus item dari koleksi saat menghitungnya, di mana biasanya diperlukan forloop untuk memulai dari elemen terakhir.
Slai
Anda mungkin mengambil nilai dari Øredev 2013 - Jessica Kerr - Prinsip Fungsional untuk Pengembang Berorientasi Objek . Linq datang ke presentasi segera setelah 33 menit, di bawah judul "Gaya Deklaratif".
Theraot

Jawaban:

139

for vs. foreach

Ada kebingungan umum bahwa kedua konstruksi ini sangat mirip dan keduanya dapat dipertukarkan seperti ini:

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

dan:

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

Fakta bahwa kedua kata kunci dimulai dengan tiga huruf yang sama tidak berarti secara semantik, keduanya serupa. Kebingungan ini sangat rawan kesalahan, terutama untuk pemula. Iterasi melalui koleksi dan melakukan sesuatu dengan elemen dilakukan dengan foreach; fortidak harus dan tidak boleh digunakan untuk tujuan ini , kecuali jika Anda benar-benar tahu apa yang Anda lakukan.

Mari kita lihat apa yang salah dengan contoh itu. Pada akhirnya, Anda akan menemukan kode lengkap aplikasi demo yang digunakan untuk mengumpulkan hasil.

Dalam contoh, kami memuat beberapa data dari basis data, lebih tepatnya kota-kota dari Adventure Works, yang dipesan dengan nama, sebelum bertemu "Boston". Query SQL berikut digunakan:

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

Data dimuat oleh ListCities()metode yang mengembalikan sebuah IEnumerable<string>. Berikut ini foreachtampilannya:

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

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

Mari kita menulis ulang dengan a for, dengan asumsi bahwa keduanya dapat dipertukarkan:

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

    Console.Write(city + " ");

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

Keduanya mengembalikan kota yang sama, tetapi ada perbedaan besar.

  • Saat menggunakan foreach, ListCities()disebut satu kali dan menghasilkan 47 item.
  • Saat menggunakan for, ListCities()disebut 94 kali dan menghasilkan 28153 item secara keseluruhan.

Apa yang terjadi?

IEnumerableadalah malas . Ini berarti bahwa itu akan melakukan pekerjaan hanya pada saat ketika hasilnya dibutuhkan. Evaluasi malas adalah konsep yang sangat berguna, tetapi memiliki beberapa peringatan, termasuk fakta bahwa mudah untuk melewatkan momen di mana hasilnya akan diperlukan, terutama dalam kasus di mana hasilnya digunakan beberapa kali.

Dalam kasus a foreach, hasilnya diminta hanya sekali. Dalam kasus a for sebagaimana diterapkan dalam kode yang ditulis secara tidak benar di atas , hasilnya diminta 94 kali , yaitu 47 × 2:

  • Setiap kali cities.Count()disebut (47 kali),

  • Setiap kali cities.ElementAt(i)disebut (47 kali).

Meminta basis data 94 kali alih-alih yang mengerikan, tetapi bukan hal terburuk yang mungkin terjadi. Bayangkan, misalnya, apa yang akan terjadi jika selectkueri akan didahului oleh kueri yang juga menyisipkan baris dalam tabel. Benar, kita akan memiliki foryang akan memanggil database 2.147.483.647 kali, kecuali semoga crash sebelumnya.

Tentu saja, kode saya bias. Saya sengaja menggunakan kemalasan IEnumerabledan menulisnya dengan cara menelepon berulang kali ListCities(). Orang dapat mencatat bahwa seorang pemula tidak akan pernah melakukan itu, karena:

  • Tidak IEnumerable<T>memiliki properti Count, tetapi hanya metodenya Count(). Memanggil metode itu menakutkan, dan orang bisa berharap hasilnya tidak akan di-cache, dan tidak cocok di for (; ...; )blok.

  • Pengindeksan tidak tersedia untuk IEnumerable<T>dan tidak jelas untuk menemukan ElementAtmetode ekstensi LINQ.

Mungkin sebagian besar pemula hanya akan mengubah hasil ListCities()menjadi sesuatu yang mereka kenal, seperti a 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;
    }
}

Meski begitu, kode ini sangat berbeda dari foreachalternatif. Sekali lagi, ini memberikan hasil yang sama, dan kali ini ListCities()metode ini dipanggil hanya sekali, tetapi menghasilkan 575 item, sementara dengan foreach, itu hanya menghasilkan 47 item.

Perbedaannya berasal dari fakta yang ToList()menyebabkan semua data dimuat dari basis data. Meskipun foreachhanya meminta kota sebelum "Boston", yang baru formeminta semua kota untuk diambil dan disimpan dalam memori. Dengan 575 string pendek, mungkin tidak ada banyak perbedaan, tetapi bagaimana jika kita mengambil hanya beberapa baris dari tabel yang berisi milyaran catatan?

Jadi apa foreachsebenarnya?

foreachlebih dekat ke loop sementara. Kode yang saya gunakan sebelumnya:

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

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

dapat dengan mudah diganti dengan:

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

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

Keduanya menghasilkan IL yang sama. Keduanya memiliki hasil yang sama. Keduanya memiliki efek samping yang sama. Tentu saja, ini whiledapat ditulis ulang dalam infinite yang serupa for, tetapi akan lebih lama dan rawan kesalahan. Anda bebas memilih yang menurut Anda lebih mudah dibaca.

Ingin mengujinya sendiri? Berikut kode lengkapnya:

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

Dan hasilnya:

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

Data disebut 94 kali dan menghasilkan 28153 item.

--- untuk daftar ---
Abingdon Albany Alexandria Alhambra [...] Bonn Bordeaux Boston

Data disebut 1 kali dan menghasilkan 575 item.

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

Data disebut 1 kali dan menghasilkan 47 item.

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

Data disebut 1 kali dan menghasilkan 47 item.

LINQ vs cara tradisional

Adapun LINQ, Anda mungkin ingin belajar pemrograman fungsional (FP) - bukan hal C # FP, tetapi bahasa FP nyata seperti Haskell. Bahasa fungsional memiliki cara khusus untuk mengekspresikan dan menyajikan kode. Dalam beberapa situasi, ini lebih unggul dari paradigma non-fungsional.

FP diketahui jauh lebih unggul dalam hal memanipulasi daftar ( daftar sebagai istilah umum, yang tidak terkait List<T>). Mengingat fakta ini, kemampuan untuk mengekspresikan kode C # dengan cara yang lebih fungsional ketika datang ke daftar agak bagus.

Jika Anda tidak yakin, bandingkan dengan keterbacaan kode yang ditulis dengan cara fungsional dan non-fungsional dalam jawaban saya sebelumnya pada subjek.

Arseni Mourzenko
sumber
1
Pertanyaan tentang contoh ListCities (). Mengapa hanya berjalan sekali? Saya tidak punya masalah untuk mencari pengembalian hasil di masa lalu.
Dante
1
Dia tidak mengatakan Anda hanya akan mendapatkan satu hasil dari IEnumerable - dia mengatakan bahwa query SQL (yang merupakan bagian mahal dari metode ini) hanya akan dieksekusi sekali - ini adalah hal yang baik. Kemudian akan membaca dan menghasilkan semua hasil dari permintaan.
HappyCat
9
@Giorgio: Meskipun pertanyaan ini dapat dimengerti, memiliki semantik bahasa menjadi sesuatu yang membingungkan bagi pemula yang tidak akan meninggalkan kita dengan bahasa yang sangat efektif.
Steven Evers
4
LINQ bukan hanya gula semantik. Ini memberikan eksekusi tertunda. Dan dalam kasus IQueryables (misalnya Entity Framework) memungkinkan kueri diteruskan dan dikomposisikan hingga iterated (artinya menambahkan klausa kemana ke IQueryable yang dikembalikan akan menghasilkan SQL yang diteruskan ke server setelah iterasi untuk menyertakan bahwa di mana klausa menonaktifkan pemfilteran ke server).
Michael Brown
8
Sama seperti saya menyukai jawaban ini, saya pikir contohnya agak dibuat-buat. Ringkasan di bagian akhir menunjukkan bahwa foreachini lebih efisien daripada for, padahal sebenarnya perbedaan tersebut adalah hasil dari kode yang sengaja dipecah. Ketelitian jawaban menebus dirinya sendiri, tetapi mudah untuk melihat bagaimana pengamat kasual mungkin sampai pada kesimpulan yang salah.
Robert Harvey
19

Meskipun sudah ada beberapa eksposisi besar tentang perbedaan antara for dan foreach. Ada beberapa kesalahan penyajian peran LINQ.

Sintaks LINQ bukan hanya gula sintaksis yang memberikan perkiraan pemrograman fungsional ke C #. LINQ menyediakan konstruksi Fungsional termasuk semua manfaatnya untuk C #. Dikombinasikan dengan mengembalikan IEnumerable dan bukan IList, LINQ menyediakan eksekusi iterasi yang ditangguhkan. Apa yang biasanya dilakukan orang sekarang adalah membuat dan mengembalikan IList dari fungsinya seperti itu

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

Alih-alih menggunakan sintaks pengembalian hasil untuk membuat enumerasi yang ditangguhkan.

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

Sekarang pencacahan tidak akan terjadi sampai Anda Daftar atau beralih di atasnya. Dan itu hanya terjadi sesuai kebutuhan (inilah enumerasi Fibbonaci yang tidak memiliki masalah stack overflow)

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

Melakukan pendahuluan atas fungsi Fibonacci akan mengembalikan urutan 46. Jika Anda ingin ke-30 itu saja yang akan dihitung

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

Di mana kita dapat bersenang-senang adalah dukungan dalam bahasa untuk ekspresi lambda (dikombinasikan dengan konstruksi IQueryable dan IQueryProvider, ini memungkinkan untuk komposisi fungsional permintaan terhadap berbagai set data, IQueryProvider bertanggung jawab untuk menafsirkan yang disahkan di ekspresi dan membuat dan mengeksekusi kueri menggunakan konstruksi asli sumber). Saya tidak akan masuk ke rincian sepele di sini, tetapi ada serangkaian posting blog yang menunjukkan cara membuat Penyedia Query SQL di sini

Singkatnya, Anda harus memilih mengembalikan IEnumerable daripada IList ketika konsumen fungsi Anda akan melakukan iterasi sederhana. Dan gunakan kapabilitas LINQ untuk menunda eksekusi query kompleks sampai dibutuhkan.

Michael Brown
sumber
13

tapi saya bisa melihat kodenya menjadi semakin kurang dapat dibaca

Keterbacaan ada di mata yang melihatnya. Beberapa orang mungkin berkata

var common = list1.Intersect(list2);

mudah dibaca; yang lain mungkin mengatakan bahwa ini buram, dan lebih suka

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

membuatnya lebih jelas apa yang sedang dilakukan. Kami tidak dapat memberi tahu Anda apa yang menurut Anda lebih mudah dibaca. Tetapi Anda mungkin dapat mendeteksi beberapa bias saya sendiri dalam contoh yang saya buat di sini ...

AakashM
sumber
28
Sejujurnya saya akan mengatakan bahwa Linq membuat niat secara obyektif lebih mudah dibaca sementara untuk loop membuat mekanisme lebih obyektif dibaca.
jk.
16
Saya akan berlari secepat mungkin dari seseorang yang memberi tahu saya bahwa versi for-for-if lebih mudah dibaca daripada versi intersect.
Konamiman
3
@Konamiman - Itu akan tergantung pada apa yang dicari seseorang ketika mereka memikirkan "keterbacaan." Komentar jk. menggambarkan ini dengan sempurna. Loop lebih mudah dibaca dalam arti bahwa Anda dapat dengan mudah melihat bagaimana mendapatkan hasil akhirnya, sementara LINQ lebih mudah dibaca dalam apa hasil akhir seharusnya.
Shauna
2
Itu sebabnya loop masuk ke implementasi, dan kemudian Anda menggunakan Intersect di mana-mana.
R. Martinho Fernandes
8
@Shauna: Bayangkan versi for-loop di dalam suatu metode melakukan beberapa hal lain; itu berantakan. Jadi, tentu saja, Anda membaginya menjadi metode sendiri. Dari segi keterbacaan, ini sama dengan IEnumerable <T> .Intersect, tetapi sekarang Anda telah menduplikasi fungsi framework dan memperkenalkan lebih banyak kode untuk dipelihara. Satu-satunya alasan adalah jika Anda memerlukan implementasi khusus untuk alasan perilaku, tetapi kami hanya berbicara tentang keterbacaan di sini.
Misko
7

Perbedaan antara LINQ dan foreachbenar - benar bermuara pada dua gaya pemrograman yang berbeda: imperatif dan deklaratif.

  • Imperatif: dengan gaya ini Anda memberi tahu komputer "lakukan ini ... sekarang lakukan ini ... sekarang lakukan ini sekarang lakukan ini". Anda memberi makan program satu langkah pada satu waktu.

  • Deklaratif: dalam gaya ini Anda memberi tahu komputer apa yang Anda inginkan hasilnya dan biarkan ia mencari cara untuk sampai ke sana.

Contoh klasik dari kedua gaya ini adalah membandingkan kode assembly (atau C) dengan SQL. Dalam pertemuan Anda memberikan instruksi (secara harfiah) satu per satu. Di SQL Anda menyatakan bagaimana cara menggabungkan data bersama dan hasil apa yang Anda inginkan dari data itu.

Efek samping yang bagus dari pemrograman deklaratif adalah bahwa ia cenderung tingkat yang sedikit lebih tinggi. Ini memungkinkan platform untuk berkembang di bawah Anda tanpa Anda harus mengubah kode. Misalnya:

var foo = bar.Distinct();

Apa yang terjadi disini? Apakah Beda menggunakan satu inti? Dua? Lima puluh Kami tidak tahu dan kami tidak peduli. .NET devs dapat menulis ulang kapan saja, selama itu terus melakukan tujuan yang sama kode kita secara ajaib bisa lebih cepat setelah pembaruan kode.

Ini adalah kekuatan pemrograman fungsional. Dan alasan Anda akan menemukan kode itu dalam bahasa seperti Clojure, F # dan C # (ditulis dengan pola pikir pemrograman fungsional) seringkali 3x-10x lebih kecil daripada rekan-rekan yang sangat penting.

Akhirnya saya suka gaya deklaratif karena di C # sebagian besar waktu ini memungkinkan saya untuk menulis kode yang tidak mengubah data. Dalam contoh di atas, Distinct()tidak mengubah bilah, ia mengembalikan salinan data baru. Ini berarti bahwa apa pun bilah itu, dan dari mana pun asalnya, tidak tiba-tiba berubah.

Jadi seperti yang dikatakan poster lainnya, pelajari pemrograman fungsional. Itu akan mengubah hidup Anda. Dan jika Anda bisa, lakukanlah dalam bahasa pemrograman fungsional yang sebenarnya. Saya lebih suka Clojure, tetapi F # dan Haskell juga pilihan yang sangat baik.

Timothy Baldridge
sumber
2
Pemrosesan LINQ ditangguhkan sampai Anda benar-benar mengulanginya. var foo = bar.Distinct()pada dasarnya adalah IEnumerator<T>sampai Anda menelepon .ToList()atau .ToArray(). Itu adalah perbedaan penting karena jika Anda tidak menyadarinya, hal itu dapat menyebabkan kesulitan untuk memahami bug.
Berin Loritsch
-5

Bisakah pengembang lain dalam tim membaca LINQ?

Jika tidak maka jangan gunakan atau salah satu dari dua hal akan terjadi:

  1. Kode Anda tidak dapat dipelihara
  2. Anda akan terjebak dengan mempertahankan semua kode Anda dan semua yang bergantung padanya

A untuk setiap loop sangat cocok untuk iterasi melalui daftar tetapi jika bukan itu yang harus Anda lakukan maka jangan gunakan satu.

Llama terbalik
sumber
11
hmm, saya menghargai bahwa untuk satu proyek, ini mungkin jawabannya, tetapi untuk jangka menengah dan panjang Anda harus melatih staf Anda, jika tidak, Anda memiliki perlombaan ke bagian bawah pemahaman kode yang sepertinya bukan ide yang bagus.
jk.
21
Sebenarnya, ada hal ketiga yang bisa terjadi: pengembang lain dapat melakukan sedikit usaha dan benar-benar mempelajari sesuatu yang baru dan bermanfaat. Itu tidak pernah terdengar.
Eric King
6
@InvertedLlama jika saya berada di sebuah perusahaan di mana para pengembang membutuhkan pelatihan formal untuk memahami konsep-konsep bahasa baru maka saya akan berpikir tentang menemukan perusahaan baru.
Wyatt Barnett
13
Mungkin Anda bisa lolos dengan sikap itu dengan perpustakaan, tetapi ketika datang ke fitur bahasa inti, itu tidak memotongnya. Anda dapat memilih dan memilih kerangka kerja. Tapi .NET programmer yang baik perlu memahami setiap fitur bahasa, dan platform inti (Sistem. *). Dan mengingat bahwa Anda bahkan tidak dapat menggunakan EF dengan benar tanpa menggunakan Linq, saya harus mengatakan ... di zaman sekarang ini, jika Anda seorang programmer .NET dan Anda tidak mengenal Linq, Anda tidak kompeten.
Timothy Baldridge
7
Ini sudah memiliki cukup banyak downvotes, jadi saya tidak akan menambahkannya, tetapi argumen yang mendukung rekan kerja yang tidak kompeten / tidak kompeten tidak pernah valid.
Steven Evers