Apa keuntungan yang diperoleh dengan mengimplementasikan LINQ dengan cara yang tidak men-cache hasil?

20

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 Recordobjek yang dihasilkan dibuat lagi. Untuk memperbaiki ini, panggilan sederhana ke ToListdapat 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?

Panzercrisis
sumber
1
Panggil saja ToList () dalam metode GenerateRecords () Anda. return listOfNames.Select(x => new Record(Guid.NewGuid(), x)).ToList(); Itu memberi Anda "salinan cache." Masalah terpecahkan.
Robert Harvey
1
Aku tahu, tapi aku bertanya-tanya mengapa mereka membuat ini penting sejak awal.
Panzercrisis
11
Karena evaluasi malas memiliki manfaat signifikan, tidak sedikit di antaranya adalah "oh, omong-omong, catatan ini berubah sejak terakhir kali Anda memintanya; inilah versi baru", yang persis seperti yang digambarkan oleh contoh kode Anda.
Robert Harvey
Saya bersumpah telah membaca pertanyaan yang hampir identik di sini dalam 6 bulan terakhir, tetapi saya tidak menemukannya sekarang. Yang paling dekat yang dapat saya temukan adalah mulai tahun 2016 di stackoverflow: stackoverflow.com/q/37437893/391656
Mr.Mindor
29
Kami memiliki nama untuk cache tanpa kebijakan kedaluwarsa: "kebocoran memori". Kami memiliki nama untuk cache tanpa kebijakan pembatalan: "bug farm". Jika Anda tidak akan mengusulkan kebijakan kedaluwarsa dan pembatalan yang selalu benar yang berfungsi untuk setiap kueri LINQ yang mungkin, maka pertanyaan Anda akan menjawab sendiri.
Eric Lippert

Jawaban:

51

Apa keuntungan yang diperoleh dengan mengimplementasikan LINQ dengan cara yang tidak men-cache hasil?

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?

IEnumerable<int> AllTheZeroes()
{
    while(true) yield return 0;
}

Kamu tidak bisa. Itu sebabnya IEnumerable<T>ada sebagaimana adanya.

tidak ada
sumber
2
Contoh terakhir Anda akan lebih menarik jika itu adalah seri tak terbatas yang sebenarnya (seperti Fibonnaci), dan bukan hanya rangkaian nol tanpa akhir, yang tidak terlalu menarik.
Robert Harvey
23
@RobertHarvey Itu benar, saya hanya berpikir lebih mudah untuk mengetahui bahwa itu adalah aliran nol tanpa akhir ketika tidak ada logika sama sekali untuk dipahami.
novigt
2
int i=1; while(true) { i++; yield fib(i); }
Robert Harvey
2
Contoh yang saya pikirkan adalah Enumerable.Range(1,int.MaxValue)- sangat mudah untuk mengerjakan batas bawah untuk berapa banyak memori yang akan digunakan.
Chris
4
Hal lain yang saya lihat di sepanjang baris while (true) return ...adalah while (true) return _random.Next();untuk menghasilkan aliran angka acak yang tak terbatas.
Chris
24

Apa keuntungan yang Microsoft harapkan untuk dapatkan dengan mengimplementasikannya dengan cara ini?

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.

Telastyn
sumber
3
Mungkin layak disebutkan bahwa ICollectionada, dan mungkin berperilaku seperti yang diharapkan OP IEnumerableuntuk berperilaku
Caleth
Jika Anda menggunakan IEnumerable <T> untuk membaca kursor database terbuka, hasil Anda tidak akan berubah jika Anda menggunakan database dengan transaksi ACID.
Doug
4

Karena 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).

Jules
sumber
4

Alasan lain yang belum disebutkan adalah, kemungkinan menggabungkan filter dan transformasi yang berbeda tanpa membuat hasil tengah sampah.

Ambil ini sebagai contoh:

cars.Where(c => c.Year > 2010)
.Select(c => new { c.Model, c.Year, c.Color })
.GroupBy(c => c.Year);

Jika metode LINQ segera menghitung hasilnya, kami akan memiliki 3 koleksi:

  • Dimana hasilnya
  • Pilih hasil
  • Hasil GroupBy

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.

Arturo Torres Sánchez
sumber
3

Pada dasarnya, kode ini - menempatkan Guid.NewGuid ()dalam sebuah Selectpernyataan - sangat mencurigakan. Ini pasti semacam bau kode!

Secara teori, kita tidak perlu mengharapkan Selectpernyataan 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 bagian NewGuid ()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.

Erik Eidt
sumber
0

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.

Ian Kemp
sumber