Ini adalah perangkap yang dikenal bagi orang-orang yang basah menggunakan LINQ:
public class Program
{
public static void Main()
{
IEnumerable<Record> originalCollection = GenerateRecords(new[] {"Jesse"});
var newCollection = new List<Record>(originalCollection);
Console.WriteLine(ContainTheSameSingleObject(originalCollection, newCollection));
}
private static IEnumerable<Record> GenerateRecords(string[] listOfNames)
{
return listOfNames.Select(x => new Record(Guid.NewGuid(), x));
}
private static bool ContainTheSameSingleObject(IEnumerable<Record>
originalCollection, List<Record> newCollection)
{
return originalCollection.Count() == 1 && newCollection.Count() == 1 &&
originalCollection.Single().Id == newCollection.Single().Id;
}
private class Record
{
public Guid Id { get; }
public string SomeValue { get; }
public Record(Guid id, string someValue)
{
Id = id;
SomeValue = someValue;
}
}
}
Ini akan mencetak "Salah", karena untuk setiap nama yang disediakan untuk membuat koleksi asli, fungsi pilih terus dievaluasi ulang, dan Record
objek yang dihasilkan dibuat lagi. Untuk memperbaiki ini, panggilan sederhana ke ToList
dapat ditambahkan di akhir GenerateRecords
.
Apa keuntungan yang Microsoft harapkan untuk dapatkan dengan mengimplementasikannya dengan cara ini?
Mengapa implementasi tidak akan hanya menyimpan hasil cache array internal? Satu bagian spesifik dari apa yang terjadi mungkin eksekusi yang ditangguhkan, tetapi itu masih bisa dilaksanakan tanpa perilaku ini.
Setelah anggota tertentu dari koleksi yang dikembalikan oleh LINQ telah dievaluasi, keuntungan apa yang diberikan dengan tidak menyimpan referensi / salinan internal, tetapi sebaliknya menghitung ulang hasil yang sama, sebagai perilaku default?
Dalam situasi di mana ada kebutuhan khusus dalam logika untuk anggota yang sama dari koleksi yang dihitung ulang berulang kali, sepertinya itu dapat ditentukan melalui parameter opsional dan bahwa perilaku default dapat melakukan sebaliknya. Selain itu, keuntungan kecepatan yang diperoleh dengan eksekusi yang ditangguhkan pada akhirnya mengurangi waktu yang diperlukan untuk terus menghitung ulang hasil yang sama. Akhirnya ini adalah blok yang membingungkan bagi mereka yang baru mengenal LINQ, dan itu dapat menyebabkan bug halus pada akhirnya program siapa pun.
Apa keuntungannya untuk ini, dan mengapa Microsoft membuat keputusan yang tampaknya sangat disengaja ini?
sumber
return listOfNames.Select(x => new Record(Guid.NewGuid(), x)).ToList();
Itu memberi Anda "salinan cache." Masalah terpecahkan.Jawaban:
Caching hasilnya tidak akan bekerja untuk semua orang. Selama Anda memiliki sejumlah kecil data, bagus. Bagus untukmu. Tetapi bagaimana jika data Anda lebih besar dari RAM Anda?
Ini tidak ada hubungannya dengan LINQ, tetapi dengan
IEnumerable<T>
antarmuka secara umum.Ini adalah perbedaan antara File.ReadAllLines dan File.ReadLines . Satu akan membaca seluruh file ke dalam RAM, dan yang lainnya akan memberikannya kepada Anda baris demi baris, sehingga Anda dapat bekerja dengan file besar (selama mereka memiliki jeda baris).
Anda dapat dengan mudah men-cache semua yang Anda ingin cache dengan mematerialisasi urutan Anda memanggil salah satu
.ToList()
atau.ToArray()
di atasnya. Tetapi kita yang tidak ingin menyimpannya, kita memiliki kesempatan untuk tidak melakukannya.Dan pada catatan terkait: bagaimana cara menyimpan yang berikut?
Kamu tidak bisa. Itu sebabnya
IEnumerable<T>
ada sebagaimana adanya.sumber
int i=1; while(true) { i++; yield fib(i); }
Enumerable.Range(1,int.MaxValue)
- sangat mudah untuk mengerjakan batas bawah untuk berapa banyak memori yang akan digunakan.while (true) return ...
adalahwhile (true) return _random.Next();
untuk menghasilkan aliran angka acak yang tak terbatas.Kebenaran? Maksudku, enumerable inti dapat berubah di antara panggilan. Caching itu akan menghasilkan hasil yang salah dan membuka seluruh "kapan / bagaimana saya membatalkan cache itu?" Can of worm.
Dan jika Anda mempertimbangkan LINQ pada awalnya dirancang sebagai sarana untuk melakukan LINQ ke sumber data (seperti entitas kerangka, atau SQL langsung), enumerable yang sedang terjadi perubahan sejak itu apa database lakukan .
Selain itu, ada kekhawatiran Prinsip Tanggung Jawab Tunggal. Jauh lebih mudah untuk membuat beberapa kode kueri yang berfungsi dan membuat cache di atasnya daripada membangun kode yang menanyakan dan menyimpan cache tetapi kemudian menghapus cache.
sumber
ICollection
ada, dan mungkin berperilaku seperti yang diharapkan OPIEnumerable
untuk berperilakuKarena LINQ, dan memang dimaksudkan sejak awal, merupakan implementasi generik dari pola Monad yang populer dalam bahasa pemrograman fungsional , dan Monad tidak dibatasi untuk selalu menghasilkan nilai yang sama dengan urutan panggilan yang sama (pada kenyataannya, penggunaannya dalam pemrograman fungsional sangat populer justru karena sifat ini, yang memungkinkan untuk melarikan diri dari perilaku deterministik fungsi murni).
sumber
Alasan lain yang belum disebutkan adalah, kemungkinan menggabungkan filter dan transformasi yang berbeda tanpa membuat hasil tengah sampah.
Ambil ini sebagai contoh:
Jika metode LINQ segera menghitung hasilnya, kami akan memiliki 3 koleksi:
Yang kami hanya peduli tentang yang terakhir. Tidak ada gunanya menyimpan hasil tengah karena kami tidak memiliki akses ke sana, dan kami hanya ingin tahu tentang mobil yang sudah difilter dan dikelompokkan berdasarkan tahun.
Jika ada kebutuhan untuk menyimpan salah satu dari hasil ini, solusinya sederhana: pisahkan panggilan dan panggil
.ToList()
mereka dan simpan dalam variabel.Sama seperti catatan tambahan, dalam JavaScript, metode Array sebenarnya mengembalikan hasilnya segera, yang dapat menyebabkan lebih banyak konsumsi memori jika seseorang tidak berhati-hati.
sumber
Pada dasarnya, kode ini - menempatkan
Guid.NewGuid ()
dalam sebuahSelect
pernyataan - sangat mencurigakan. Ini pasti semacam bau kode!Secara teori, kita tidak perlu mengharapkan
Select
pernyataan untuk membuat data baru tetapi untuk mengambil data yang ada. Meskipun Masuk akal untuk menggabungkan data dari berbagai sumber untuk menghasilkan konten yang digabung dengan bentuk yang berbeda atau bahkan menghitung kolom tambahan, kami mungkin masih mengharapkannya berfungsi & murni. Menempatkan bagianNewGuid ()
dalam membuatnya tidak fungsional & tidak murni.Pembuatan data dapat diejek terpisah dari seleksi dan dimasukkan ke dalam semacam operasi buat, sehingga pilih dapat tetap murni dan dapat digunakan kembali, atau pemilihan harus dilakukan hanya sekali dan dibungkus / dilindungi - ini adalah
.ToList ()
sarannya.Namun, untuk lebih jelasnya, masalah ini bagi saya tampaknya adalah pencampuran ciptaan di dalam seleksi daripada kurangnya caching. Menempatkan bagian
NewGuid()
dalam pilih bagi saya menjadi campuran yang tidak tepat dari model pemrograman.sumber
Eksekusi yang ditangguhkan memungkinkan mereka yang menulis kode LINQ (tepatnya, menggunakan
IEnumerable<T>
) untuk secara eksplisit memilih apakah hasilnya segera dihitung dan disimpan dalam memori, atau tidak. Dengan kata lain, ini memungkinkan programmer untuk memilih waktu perhitungan versus tradeoff ruang penyimpanan yang paling sesuai dengan aplikasi mereka.Dapat dikatakan bahwa sebagian besar aplikasi menginginkan hasil segera, sehingga seharusnya menjadi perilaku default LINQ. Tetapi ada banyak API lain (misalnya
List<T>.ConvertAll
) yang menawarkan perilaku ini dan telah dilakukan sejak Kerangka dibuat, sedangkan sampai LINQ diperkenalkan, tidak ada cara untuk menunda eksekusi. Yang, seperti yang ditunjukkan oleh jawaban lain, merupakan prasyarat untuk mengaktifkan jenis komputasi tertentu yang sebaliknya mustahil (dengan menghabiskan semua penyimpanan yang tersedia) saat menggunakan eksekusi segera.sumber