foreach vs someList.ForEach () {}

167

Tampaknya ada banyak cara untuk beralih pada koleksi. Ingin tahu apakah ada perbedaan, atau mengapa Anda menggunakan satu cara di atas yang lain.

Tipe pertama:

List<string> someList = <some way to init>
foreach(string s in someList) {
   <process the string>
}

Cara lain:

List<string> someList = <some way to init>
someList.ForEach(delegate(string s) {
    <process the string>
});

Saya kira dari atas kepala saya, bahwa alih-alih delegasi anonim yang saya gunakan di atas, Anda akan memiliki delegasi yang dapat digunakan kembali yang dapat Anda tentukan ...

Bryce Fischer
sumber
1
Saya sarankan membaca blog Eric Lipperts "foreach" vs "ForEach"
Erik Philips

Jawaban:

134

Ada satu perbedaan penting dan berguna antara keduanya.

Karena .ForEach menggunakan forloop untuk mengulang koleksi, ini valid (sunting: sebelum .net 4.5 - implementasinya berubah dan mereka berdua melempar):

someList.ForEach(x => { if(x.RemoveMe) someList.Remove(x); }); 

sedangkan foreachmenggunakan enumerator, jadi ini tidak valid:

foreach(var item in someList)
  if(item.RemoveMe) someList.Remove(item);

tl; dr: JANGAN menyalin kode ini ke dalam aplikasi Anda!

Contoh-contoh ini bukan praktik terbaik, mereka hanya untuk menunjukkan perbedaan antara ForEach()dan foreach.

Menghapus item dari daftar dalam satu forlingkaran dapat memiliki efek samping. Yang paling umum dijelaskan dalam komentar untuk pertanyaan ini.

Secara umum, jika Anda ingin menghapus beberapa item dari daftar, Anda ingin memisahkan penentuan item mana yang akan dihapus dari penghapusan yang sebenarnya. Itu tidak membuat kode Anda kompak, tetapi itu menjamin bahwa Anda tidak kehilangan barang apa pun.


sumber
35
bahkan kemudian, Anda harus menggunakan someList.RemoveAll (x => x.RemoveMe) sebagai gantinya
Mark Cidade
2
Dengan Linq, semua hal dapat dilakukan dengan lebih baik. Saya baru saja menunjukkan contoh memodifikasi koleksi dalam foreach ...
2
RemoveAll () adalah metode pada Daftar <T>.
Mark Cidade
3
Anda mungkin menyadari hal ini, tetapi orang-orang harus berhati-hati menghapus item dengan cara ini; jika Anda menghapus item N maka iterasi akan melewati item (N +1) dan Anda tidak akan melihatnya di delegasi Anda atau mendapatkan kesempatan untuk menghapusnya, sama seperti jika Anda melakukannya sendiri untuk loop.
El Zorko
9
Jika Anda iterate daftar mundur dengan untuk loop, Anda dapat menghapus item tanpa masalah indeks-shift.
S. Tarık Çetin
78

Kami memiliki beberapa kode di sini (dalam VS2005 dan C # 2.0) di mana para insinyur sebelumnya menggunakan cara mereka list.ForEach( delegate(item) { foo;});alih-alih foreach(item in list) {foo; };untuk semua kode yang mereka tulis. misalnya satu blok kode untuk membaca baris dari dataReader.

Saya masih tidak tahu persis mengapa mereka melakukan ini.

Kelemahannya list.ForEach()adalah:

  • Itu lebih verbose di C # 2.0. Namun, dalam C # 3 dan seterusnya, Anda dapat menggunakan =>sintaksis " " untuk membuat beberapa ekspresi yang tepat.

  • Itu kurang akrab. Orang yang harus mempertahankan kode ini akan bertanya-tanya mengapa Anda melakukannya dengan cara itu. Butuh beberapa saat untuk memutuskan bahwa tidak ada alasan, kecuali mungkin untuk membuat penulis tampak pintar (kualitas sisa kode merusak itu). Itu juga kurang dapat dibaca, dengan " })" di akhir blok kode delegasi.

  • Lihat juga buku Bill Wagner "Efektif C #: 50 Cara Khusus untuk Meningkatkan C # Anda" di mana ia berbicara tentang mengapa foreach lebih disukai daripada loop lain seperti untuk atau sementara loop - poin utamanya adalah bahwa Anda membiarkan kompiler memutuskan cara terbaik untuk membangun putaran. Jika versi kompiler di masa depan berhasil menggunakan teknik yang lebih cepat, maka Anda akan mendapatkannya secara gratis dengan menggunakan foreach dan membangun kembali, daripada mengubah kode Anda.

  • sebuah foreach(item in list)konstruk memungkinkan Anda untuk menggunakan breakatau continuejika Anda harus keluar dari iterasi atau loop. Tetapi Anda tidak dapat mengubah daftar di dalam foreach loop.

Saya terkejut melihat itu list.ForEachsedikit lebih cepat. Tapi itu mungkin bukan alasan yang valid untuk menggunakannya, itu akan menjadi optimasi prematur. Jika aplikasi Anda menggunakan database atau layanan web yang, bukan kontrol loop, hampir selalu akan berada di tempat waktu berlalu. Dan apakah Anda telah membandingkannya dengan forloop juga? The list.ForEachbisa lebih cepat karena menggunakan yang internal dan forlingkaran tanpa pembungkus akan lebih cepat.

Saya tidak setuju bahwa list.ForEach(delegate)versi "lebih fungsional" dengan cara apa pun yang signifikan. Itu melewati fungsi ke fungsi, tetapi tidak ada perbedaan besar dalam hasil atau program organisasi.

Saya tidak berpikir bahwa foreach(item in list)"mengatakan dengan tepat bagaimana Anda menginginkannya dilakukan" - satu for(int 1 = 0; i < count; i++)loop melakukan itu, satu foreachloop meninggalkan pilihan kontrol hingga ke kompiler.

Perasaan saya adalah, pada proyek baru, digunakan foreach(item in list)untuk sebagian besar loop untuk mematuhi penggunaan umum dan untuk keterbacaan, dan list.Foreach()hanya digunakan untuk blok pendek, ketika Anda dapat melakukan sesuatu yang lebih elegan atau kompak dengan =>operator C # 3 " ". Dalam kasus seperti itu, mungkin sudah ada metode ekstensi LINQ yang lebih spesifik daripada ForEach(). Lihat apakah Where(), Select(), Any(), All(), Max()atau salah satu dari banyak metode LINQ lain tidak sudah melakukan apa yang Anda inginkan dari loop.

Anthony
sumber
Hanya untuk rasa ingin tahu ... lihat implementasi Microsoft ... referenceource.microsoft.com/#mscorlib/system/collections/…
Alexandre
17

Untuk bersenang-senang, saya memasukkan Daftar ke reflektor dan ini adalah hasil C #:

public void ForEach(Action<T> action)
{
    if (action == null)
    {
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
    }
    for (int i = 0; i < this._size; i++)
    {
        action(this._items[i]);
    }
}

Demikian pula, MoveNext di Enumerator yang digunakan oleh foreach adalah sebagai berikut:

public bool MoveNext()
{
    if (this.version != this.list._version)
    {
        ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
    }
    if (this.index < this.list._size)
    {
        this.current = this.list._items[this.index];
        this.index++;
        return true;
    }
    this.index = this.list._size + 1;
    this.current = default(T);
    return false;
}

Daftar. FOREach jauh lebih dipangkas daripada MoveNext - pemrosesan jauh lebih sedikit - akan lebih mungkin JIT menjadi sesuatu yang efisien ..

Selain itu, foreach () akan mengalokasikan Enumerator baru apa pun yang terjadi. GC adalah teman Anda, tetapi jika Anda melakukan hal yang sama berulang kali, ini akan membuat lebih banyak objek yang dibuang, dibandingkan menggunakan kembali delegasi yang sama - TETAPI - ini benar-benar kasus pinggiran. Dalam penggunaan umum Anda akan melihat sedikit atau tidak ada perbedaan.

alas tiang
sumber
1
Anda tidak memiliki jaminan bahwa kode yang dihasilkan oleh foreach akan sama antara versi kompiler. Kode yang dihasilkan dapat ditingkatkan dengan versi yang akan datang.
Anthony
1
Sampai dengan .NET Core 3.1 ForEach masih lebih cepat.
jackmott
13

Saya tahu dua hal yang tidak jelas yang membuat mereka berbeda. Pergi aku!

Pertama, ada bug klasik membuat delegasi untuk setiap item dalam daftar. Jika Anda menggunakan kata kunci foreach, semua delegasi Anda dapat merujuk pada item terakhir dari daftar:

    // A list of actions to execute later
    List<Action> actions = new List<Action>();

    // Numbers 0 to 9
    List<int> numbers = Enumerable.Range(0, 10).ToList();

    // Store an action that prints each number (WRONG!)
    foreach (int number in numbers)
        actions.Add(() => Console.WriteLine(number));

    // Run the actions, we actually print 10 copies of "9"
    foreach (Action action in actions)
        action();

    // So try again
    actions.Clear();

    // Store an action that prints each number (RIGHT!)
    numbers.ForEach(number =>
        actions.Add(() => Console.WriteLine(number)));

    // Run the actions
    foreach (Action action in actions)
        action();

Metode List.ForEach tidak memiliki masalah ini. Item iterasi saat ini diteruskan oleh nilai sebagai argumen ke lambda luar, dan kemudian lambda dalam menangkap dengan benar argumen itu dalam penutupannya sendiri. Masalah terpecahkan.

(Sedihnya saya percaya ForEach adalah anggota List, daripada metode ekstensi, meskipun mudah untuk mendefinisikannya sendiri sehingga Anda memiliki fasilitas ini pada jenis enumerable apa pun.)

Kedua, pendekatan metode ForEach memiliki keterbatasan. Jika Anda menerapkan IEnumerable dengan menggunakan pengembalian hasil, Anda tidak bisa melakukan pengembalian hasil di dalam lambda. Jadi perulangan item dalam koleksi untuk menghasilkan hal-hal pengembalian tidak mungkin dengan metode ini. Anda harus menggunakan kata kunci foreach dan mengatasi masalah penutupan dengan secara manual membuat salinan nilai loop saat ini di dalam loop.

Lebih lanjut di sini

Daniel Earwicker
sumber
5
Masalah yang Anda sebutkan foreachadalah "diperbaiki" di C # 5. stackoverflow.com/questions/8898925/…
StriplingWarrior
13

Saya kira someList.ForEach()panggilan bisa dengan mudah diparalelkan sedangkan yang normal foreachtidak mudah dijalankan paralel. Anda dapat dengan mudah menjalankan beberapa delegasi yang berbeda pada core yang berbeda, yang tidak mudah dilakukan dengan normalforeach .
Hanya 2 sen saya

Joachim Kerschbaumer
sumber
2
Saya pikir maksudnya mesin runtime dapat memparalelkannya secara otomatis. Jika tidak, baik foreach dan .ForEach dapat diparalelkan dengan tangan menggunakan utas dari kelompok di setiap delegasi aksi
Isak Savo
@ Isak tapi itu akan menjadi asumsi yang salah. Jika metode anonim mengangkat anggota lokal atau anggota, runtime tidak akan dengan mudah) dapat melumpuhkan
Rune FS
6

Seperti yang mereka katakan, iblis ada di detail ...

Perbedaan terbesar antara dua metode pencacahan koleksi adalah yang foreachmembawa negara, sedangkan ForEach(x => { })tidak.

Tetapi mari kita gali lebih dalam, karena ada beberapa hal yang harus Anda sadari yang dapat memengaruhi keputusan Anda, dan ada beberapa peringatan yang harus Anda waspadai ketika melakukan koding untuk kedua kasus.

Mari kita gunakan List<T>dalam percobaan kecil kami untuk mengamati perilaku. Untuk percobaan ini, saya menggunakan .NET 4.7.2:

var names = new List<string>
{
    "Henry",
    "Shirley",
    "Ann",
    "Peter",
    "Nancy"
};

Mari kita beralih dari ini dengan yang foreachpertama:

foreach (var name in names)
{
    Console.WriteLine(name);
}

Kami dapat memperluas ini ke:

using (var enumerator = names.GetEnumerator())
{

}

Dengan enumerator di tangan, mencari di bawah selimut kita dapatkan:

public List<T>.Enumerator GetEnumerator()
{
  return new List<T>.Enumerator(this);
}
    internal Enumerator(List<T> list)
{
  this.list = list;
  this.index = 0;
  this.version = list._version;
  this.current = default (T);
}

public bool MoveNext()
{
  List<T> list = this.list;
  if (this.version != list._version || (uint) this.index >= (uint) list._size)
    return this.MoveNextRare();
  this.current = list._items[this.index];
  ++this.index;
  return true;
}

object IEnumerator.Current
{
  {
    if (this.index == 0 || this.index == this.list._size + 1)
      ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumOpCantHappen);
    return (object) this.Current;
  }
}

Dua hal menjadi bukti langsung:

  1. Kami dikembalikan objek stateful dengan pengetahuan mendalam tentang koleksi yang mendasarinya.
  2. Salinan koleksi adalah salinan dangkal.

Ini tentu saja tidak aman. Seperti yang ditunjukkan di atas, mengubah koleksi saat iterasi hanya mojo yang buruk.

Tetapi bagaimana dengan masalah koleksi yang menjadi tidak valid selama iterasi dengan cara di luar dari kita mucking dengan koleksi selama iterasi? Praktik terbaik menyarankan untuk memvariasikan koleksi selama operasi dan iterasi, dan memeriksa versi untuk mendeteksi kapan koleksi yang mendasarinya berubah.

Di sinilah segalanya menjadi sangat keruh. Menurut dokumentasi Microsoft:

Jika perubahan dilakukan pada koleksi, seperti menambahkan, memodifikasi, atau menghapus elemen, perilaku enumerator tidak ditentukan.

Nah, apa artinya itu? Sebagai contoh, hanya karena List<T>mengimplementasikan penanganan pengecualian tidak berarti bahwa semua koleksi yang mengimplementasikan IList<T>akan melakukan hal yang sama. Itu tampaknya merupakan pelanggaran yang jelas terhadap Prinsip Pergantian Liskov:

Objek superclass harus diganti dengan objek subclassnya tanpa merusak aplikasi.

Masalah lain adalah bahwa enumerator harus mengimplementasikan IDisposable- yang berarti sumber kebocoran memori potensial, tidak hanya jika penelepon salah, tetapi jika penulis tidak menerapkan Disposepola dengan benar.

Terakhir, kami memiliki masalah seumur hidup ... apa yang terjadi jika iterator valid, tetapi koleksi yang mendasarinya hilang? Kami sekarang snapshot dari apa yang ... ketika Anda memisahkan koleksi seumur hidup dan iteratornya, Anda meminta masalah.

Sekarang mari kita periksa ForEach(x => { }):

names.ForEach(name =>
{

});

Ini berkembang menjadi:

public void ForEach(Action<T> action)
{
  if (action == null)
    ThrowHelper.ThrowArgumentNullException(ExceptionArgument.match);
  int version = this._version;
  for (int index = 0; index < this._size && (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5); ++index)
    action(this._items[index]);
  if (version == this._version || !BinaryCompatibility.TargetsAtLeast_Desktop_V4_5)
    return;
  ThrowHelper.ThrowInvalidOperationException(ExceptionResource.InvalidOperation_EnumFailedVersion);
}

Yang penting diperhatikan adalah sebagai berikut:

for (int index = 0; index < this._size && ... ; ++index) action(this._items[index]);

Kode ini tidak mengalokasikan enumerator apa pun (tidak ada ke Dispose), dan tidak berhenti saat iterasi.

Perhatikan bahwa ini juga melakukan salinan dangkal dari koleksi yang mendasarinya, tetapi koleksi sekarang menjadi snapshot dalam waktu. Jika penulis tidak benar menerapkan cek untuk koleksi yang berubah atau menjadi 'basi', foto itu masih valid.

Ini sama sekali tidak melindungi Anda dari masalah masalah seumur hidup ... jika koleksi yang mendasarinya hilang, Anda sekarang memiliki salinan dangkal yang menunjuk ke apa yang ... tetapi setidaknya Anda tidak memiliki Dispose masalah untuk berurusan dengan iterator yatim ...

Ya, saya katakan iterators ... kadang-kadang menguntungkan untuk memiliki keadaan. Misalkan Anda ingin mempertahankan sesuatu yang mirip dengan kursor basis data ... mungkin beberapa foreachgayaIterator<T> adalah cara untuk pergi. Saya pribadi tidak suka gaya desain ini karena ada terlalu banyak masalah seumur hidup, dan Anda mengandalkan rahmat baik dari penulis koleksi yang Anda andalkan (kecuali jika Anda benar-benar menulis semuanya sendiri dari awal).

Selalu ada opsi ketiga ...

for (var i = 0; i < names.Count; i++)
{
    Console.WriteLine(names[i]);
}

Ini tidak seksi, tetapi giginya tajam (permintaan maaf kepada Tom Cruise dan film The Firm )

Ini pilihan Anda, tetapi sekarang Anda tahu dan itu bisa menjadi pilihan.

Stacy Dudovitz
sumber
5

Anda bisa memberi nama delegasi anonim :-)

Dan Anda dapat menulis yang kedua sebagai:

someList.ForEach(s => s.ToUpper())

Yang saya sukai, dan menyimpan banyak pengetikan.

Seperti yang dikatakan Joachim, paralelisme lebih mudah diterapkan pada bentuk kedua.

Craig.Nicol
sumber
2

Di belakang layar, delegasi anonim akan berubah menjadi metode aktual sehingga Anda dapat memiliki beberapa overhead dengan pilihan kedua jika kompiler tidak memilih untuk sebaris fungsi. Selain itu, variabel lokal apa pun yang dirujuk oleh badan contoh delegasi anonim akan berubah sifatnya karena trik kompiler untuk menyembunyikan fakta bahwa ia dikompilasi ke metode baru. Info lebih lanjut di sini tentang bagaimana C # melakukan sihir ini:

http://blogs.msdn.com/oldnewthing/archive/2006/08/04/688527.aspx

jezell
sumber
2

Fungsi ForEach adalah anggota Daftar kelas generik.

Saya telah membuat ekstensi berikut untuk mereproduksi kode internal:

public static class MyExtension<T>
    {
        public static void MyForEach(this IEnumerable<T> collection, Action<T> action)
        {
            foreach (T item in collection)
                action.Invoke(item);
        }
    }

Jadi akhirnya kita menggunakan foreach normal (atau loop jika Anda mau).

Di sisi lain, menggunakan fungsi delegasi hanyalah cara lain untuk mendefinisikan suatu fungsi, kode ini:

delegate(string s) {
    <process the string>
}

setara dengan:

private static void myFunction(string s, <other variables...>)
{
   <process the string>
}

atau menggunakan ekspresi labda:

(s) => <process the string>
Pablo Caballero
sumber
2

Seluruh lingkup ForEach (fungsi delegasi) diperlakukan sebagai satu baris kode (memanggil fungsi), dan Anda tidak dapat mengatur breakpoint atau masuk ke dalam kode. Jika pengecualian terjadi, seluruh blok ditandai.

Peter Shen
sumber
2

List.ForEach () dianggap lebih fungsional.

List.ForEach()mengatakan apa yang ingin kamu lakukan. foreach(item in list)juga mengatakan persis bagaimana Anda ingin itu dilakukan. Ini List.ForEachbebas untuk mengubah implementasi bagian bagaimana di masa depan. Misalnya, versi .Net hipotetis di masa depan mungkin selalu berjalanList.ForEach secara paralel, dengan asumsi bahwa pada titik ini setiap orang memiliki sejumlah inti cpu yang umumnya tidak digunakan.

Di sisi lain, foreach (item in list)memberi Anda sedikit lebih banyak kontrol atas loop. Misalnya, Anda tahu bahwa item akan diulang dalam beberapa jenis urutan berurutan, dan Anda bisa dengan mudah pecah di tengah jika suatu item memenuhi beberapa kondisi.


Beberapa komentar terbaru tentang masalah ini tersedia di sini:

https://stackoverflow.com/a/529197/3043

Joel Coehoorn
sumber
1

Cara kedua yang Anda tunjukkan menggunakan metode ekstensi untuk menjalankan metode delegasi untuk setiap elemen dalam daftar.

Dengan cara ini, Anda memiliki panggilan delegasi (= metode) lain.

Selain itu, ada kemungkinan untuk mengulang daftar dengan loop for .

EFrank
sumber
1

Satu hal yang perlu diwaspadai adalah bagaimana keluar dari metode .ForEach Generik - lihat diskusi ini . Meskipun tautan sepertinya mengatakan bahwa cara ini adalah yang tercepat. Tidak yakin mengapa - Anda akan berpikir mereka akan setara setelah dikompilasi ...

Chris Kimpton
sumber
0

Ada cara, saya telah melakukannya di aplikasi saya:

List<string> someList = <some way to init>
someList.ForEach(s => <your actions>);

Anda dapat menggunakan sebagai item Anda dari foreach

Dannick Bédard
sumber