Seperti yang mereka katakan, iblis ada di detail ...
Perbedaan terbesar antara dua metode pencacahan koleksi adalah yang foreach
membawa 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 foreach
pertama:
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:
- Kami dikembalikan objek stateful dengan pengetahuan mendalam tentang koleksi yang mendasarinya.
- 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 Dispose
pola 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 foreach
gayaIterator<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.