Apa yang tidak bisa dilakukan Expression.Quote () yang Expression.Constant () itu?

98

Catatan: Saya mengetahui pertanyaan sebelumnya “ Apa tujuan metode Expression.Quote LINQ? , Tetapi jika Anda membaca terus, Anda akan melihat bahwa itu tidak menjawab pertanyaan saya.

Saya mengerti apa tujuan yang dinyatakan Expression.Quote(). Namun, Expression.Constant()dapat digunakan untuk tujuan yang sama (selain untuk semua tujuan yang Expression.Constant()telah digunakan sebelumnya). Oleh karena itu, saya tidak mengerti mengapa Expression.Quote()sama sekali diperlukan.

Untuk mendemonstrasikan ini, saya telah menulis contoh singkat di mana seseorang biasanya menggunakan Quote(lihat baris yang ditandai dengan tanda seru), tetapi saya menggunakan Constantsebagai gantinya dan itu bekerja dengan baik:

string[] array = { "one", "two", "three" };

// This example constructs an expression tree equivalent to the lambda:
// str => str.AsQueryable().Any(ch => ch == 'e')

Expression<Func<char, bool>> innerLambda = ch => ch == 'e';

var str = Expression.Parameter(typeof(string), "str");
var expr =
    Expression.Lambda<Func<string, bool>>(
        Expression.Call(typeof(Queryable), "Any", new Type[] { typeof(char) },
            Expression.Call(typeof(Queryable), "AsQueryable",
                            new Type[] { typeof(char) }, str),
            // !!!
            Expression.Constant(innerLambda)    // <--- !!!
        ),
        str
    );

// Works like a charm (prints one and three)
foreach (var str in array.AsQueryable().Where(expr))
    Console.WriteLine(str);

Output dari expr.ToString()keduanya juga sama (baik saya gunakan Constantatau Quote).

Mengingat pengamatan di atas, tampaknya itu Expression.Quote()mubazir. Kompilator C # bisa saja dibuat untuk mengkompilasi ekspresi lambda bersarang menjadi pohon ekspresi yang melibatkan Expression.Constant()alih - alih Expression.Quote(), dan penyedia kueri LINQ apa ​​pun yang ingin memproses pohon ekspresi ke dalam beberapa bahasa kueri lain (seperti SQL) dapat mencari ConstantExpressiondengan tipe Expression<TDelegate>daripada a UnaryExpressiondengan Quotetipe simpul khusus , dan yang lainnya akan sama.

Apa yang saya lewatkan? Mengapa Expression.Quote()dan Quotejenis node khusus untuk UnaryExpressionditemukan?

Timwi
sumber

Jawaban:

189

Jawaban singkat:

Operator kutipan adalah operator yang menginduksi semantik penutupan pada operannya . Konstanta hanyalah nilai.

Kutipan dan konstanta memiliki arti yang berbeda dan oleh karena itu memiliki representasi berbeda dalam pohon ekspresi . Memiliki representasi yang sama untuk dua hal yang sangat berbeda sangat membingungkan dan rentan bug.

Jawaban panjang:

Pertimbangkan hal berikut:

(int s)=>(int t)=>s+t

Lambda luar adalah pabrik untuk penambah yang terikat ke parameter lambda luar.

Sekarang, misalkan kita ingin menampilkan ini sebagai pohon ekspresi yang nantinya akan dikompilasi dan dijalankan. Seharusnya tubuh pohon ekspresi itu seperti apa? Itu tergantung pada apakah Anda ingin status yang dikompilasi mengembalikan delegasi atau pohon ekspresi.

Mari kita mulai dengan mengabaikan kasus yang tidak menarik. Jika kami ingin mengembalikan delegasi, maka pertanyaan apakah akan menggunakan Kutipan atau Konstan adalah poin yang diperdebatkan:

        var ps = Expression.Parameter(typeof(int), "s");
        var pt = Expression.Parameter(typeof(int), "t");
        var ex1 = Expression.Lambda(
                Expression.Lambda(
                    Expression.Add(ps, pt),
                pt),
            ps);

        var f1a = (Func<int, Func<int, int>>) ex1.Compile();
        var f1b = f1a(100);
        Console.WriteLine(f1b(123));

Lambda memiliki lambda bersarang; kompilator menghasilkan lambda interior sebagai delegasi ke fungsi yang ditutup di atas status fungsi yang dihasilkan untuk lambda luar. Kasus ini tidak perlu kita pertimbangkan lagi.

Misalkan kita ingin keadaan yang dikompilasi mengembalikan pohon ekspresi interior. Ada dua cara untuk melakukannya: cara mudah dan cara sulit.

Cara yang sulit adalah mengatakannya, bukan

(int s)=>(int t)=>s+t

yang kami maksud adalah

(int s)=>Expression.Lambda(Expression.Add(...

Dan kemudian buat pohon ekspresi untuk itu , menghasilkan kekacauan ini :

        Expression.Lambda(
            Expression.Call(typeof(Expression).GetMethod("Lambda", ...

bla bla bla, lusinan baris kode refleksi untuk membuat lambda. Tujuan dari operator kutipan adalah untuk memberi tahu penyusun pohon ekspresi bahwa kita ingin lambda yang diberikan diperlakukan sebagai pohon ekspresi, bukan sebagai fungsi, tanpa harus secara eksplisit menghasilkan kode pembuatan pohon ekspresi .

Cara mudahnya adalah:

        var ex2 = Expression.Lambda(
            Expression.Quote(
                Expression.Lambda(
                    Expression.Add(ps, pt),
                pt)),
            ps);

        var f2a = (Func<int, Expression<Func<int, int>>>)ex2.Compile();
        var f2b = f2a(200).Compile();
        Console.WriteLine(f2b(123));

Dan memang, jika Anda mengkompilasi dan menjalankan kode ini, Anda mendapatkan jawaban yang benar.

Perhatikan bahwa operator kutipan adalah operator yang menginduksi semantik penutupan pada lambda interior yang menggunakan variabel luar, parameter formal dari lambda luar.

Pertanyaannya adalah: mengapa tidak menghilangkan Kutipan dan membuat ini melakukan hal yang sama?

        var ex3 = Expression.Lambda(
            Expression.Constant(
                Expression.Lambda(
                    Expression.Add(ps, pt),
                pt)),
            ps);

        var f3a = (Func<int, Expression<Func<int, int>>>)ex3.Compile();
        var f3b = f3a(300).Compile();
        Console.WriteLine(f3b(123));

Konstanta tidak menyebabkan semantik penutupan. Kenapa harus begitu? Anda mengatakan bahwa ini adalah sebuah konstanta . Itu hanya sebuah nilai. Ini harus sempurna saat diserahkan ke kompiler; compiler harus dapat menghasilkan dump nilai tersebut ke stack yang dibutuhkan.

Karena tidak ada penutupan yang diinduksi, jika Anda melakukan ini, Anda akan mendapatkan pengecualian "variabel 'tipe' System.Int32 'tidak ditentukan" pada pemanggilan.

(Selain: Saya baru saja meninjau generator kode untuk pembuatan delegasi dari pohon ekspresi yang dikutip, dan sayangnya komentar yang saya masukkan ke dalam kode pada tahun 2006 masih ada. FYI, parameter luar yang diangkat snapshotted menjadi sebuah konstanta ketika dikutip pohon ekspresi direifikasi sebagai delegasi oleh runtime compiler.Ada alasan bagus mengapa saya menulis kode seperti itu yang tidak saya ingat pada saat ini, tetapi memiliki efek samping yang buruk yaitu memperkenalkan penutupan atas nilai parameter luar daripada menutup variabel. Rupanya tim yang mewarisi kode itu memutuskan untuk tidak memperbaiki cacat itu, jadi jika Anda mengandalkan mutasi parameter luar tertutup yang diamati dalam lambda interior yang dikutip terkompilasi, Anda akan kecewa. Namun, karena ini adalah praktik pemrograman yang sangat buruk untuk (1) mengubah parameter formal dan (2) bergantung pada mutasi variabel luar, saya sarankan Anda mengubah program Anda untuk tidak menggunakan dua praktik pemrograman yang buruk ini, daripada menunggu perbaikan yang tampaknya tidak akan datang. Maaf atas kesalahannya.)

Jadi, ulangi pertanyaannya:

Kompiler C # bisa saja dibuat untuk mengkompilasi ekspresi lambda bersarang ke dalam pohon ekspresi yang melibatkan Expression.Constant () daripada Expression.Quote (), dan penyedia kueri LINQ apa ​​pun yang ingin memproses pohon ekspresi ke dalam beberapa bahasa kueri lain (seperti SQL ) bisa mencari ConstantExpression dengan tipe Expression daripada UnaryExpression dengan tipe node Kutipan khusus, dan yang lainnya akan sama.

Anda benar. Kita bisa menyandikan informasi semantik yang berarti "menyebabkan penutupan semantik pada nilai ini" dengan menggunakan tipe ekspresi konstan sebagai sebuah tanda .

"Konstanta" kemudian akan memiliki arti "menggunakan nilai konstanta ini, kecuali jika jenisnya adalah jenis pohon ekspresi dan nilainya adalah pohon ekspresi yang valid, dalam hal ini, gunakan nilai yang merupakan pohon ekspresi yang dihasilkan dari penulisan ulang interior pohon ekspresi yang diberikan untuk menginduksi semantik penutupan dalam konteks lambda luar mana pun yang mungkin kita berada sekarang.

Tapi mengapa akan kita melakukan hal yang gila? Operator kutipan adalah operator yang sangat rumit , dan harus digunakan secara eksplisit jika Anda akan menggunakannya. Anda menyarankan agar agar lebih hemat tentang tidak menambahkan satu metode pabrik tambahan dan jenis node di antara beberapa lusin yang sudah ada di sana, kami menambahkan kasus sudut yang aneh ke konstanta, sehingga konstanta terkadang secara logis konstan, dan terkadang ditulis ulang lambda dengan semantik penutupan.

Ini juga akan memiliki efek yang agak aneh bahwa konstanta tidak berarti "gunakan nilai ini". Misalkan untuk beberapa alasan aneh Anda ingin kasus ketiga di atas mengkompilasi pohon ekspresi menjadi delegasi yang membagikan pohon ekspresi yang memiliki referensi yang tidak ditulis ulang ke variabel luar? Mengapa? Mungkin karena Anda menguji kompiler dan ingin meneruskan konstanta sehingga Anda dapat melakukan analisis lain nanti. Proposal Anda akan membuat itu tidak mungkin; setiap konstanta yang kebetulan merupakan tipe pohon ekspresi akan ditulis ulang. Seseorang memiliki harapan yang masuk akal bahwa "konstan" berarti "menggunakan nilai ini". "Constant" adalah node "lakukan apa yang saya katakan". Prosesor konstan ' untuk mengatakan berdasarkan jenisnya.

Dan tentu saja perhatikan bahwa Anda sekarang meletakkan beban pemahaman (yaitu, memahami bahwa konstanta memiliki semantik rumit yang berarti "konstan" dalam satu kasus dan "menyebabkan semantik penutupan" berdasarkan bendera yang ada dalam sistem tipe ) pada setiap penyedia yang melakukan analisis semantik pohon ekspresi, tidak hanya pada penyedia Microsoft. Berapa banyak dari penyedia pihak ketiga itu yang salah?

"Kutipan" melambai-lambaikan bendera merah besar yang bertuliskan "hai sobat, lihat ke sini, saya adalah ekspresi lambda bersarang dan saya memiliki semantik aneh jika saya menutup variabel luar!" sedangkan "Konstan" mengatakan "Saya tidak lebih dari nilai; gunakan saya sesuai keinginan Anda." Ketika ada sesuatu yang rumit dan berbahaya, kami ingin membuatnya mengibarkan bendera merah, tidak menyembunyikan fakta itu dengan membuat pengguna menggali melalui sistem tipe untuk mengetahui apakah nilai ini istimewa atau tidak.

Selain itu, gagasan bahwa menghindari redundansi bahkan merupakan tujuan adalah salah. Tentu, menghindari redundansi yang tidak perlu dan membingungkan adalah sebuah tujuan, tetapi kebanyakan redundansi adalah hal yang baik; redundansi menciptakan kejelasan. Metode pabrik baru dan jenis node murah . Kami dapat membuat sebanyak yang kami butuhkan sehingga masing-masing mewakili satu operasi dengan rapi. Kita tidak perlu menggunakan trik jahat seperti "ini berarti satu hal kecuali bidang ini disetel ke benda ini, dalam hal ini berarti sesuatu yang lain."

Eric Lippert
sumber
11
Saya malu sekarang karena saya tidak memikirkan semantik penutupan dan gagal menguji kasus di mana lambda bersarang menangkap parameter dari lambda luar. Jika saya melakukan itu, saya akan melihat perbedaannya. Terima kasih banyak lagi atas jawaban Anda.
Timwi
19

Pertanyaan ini sudah mendapatkan jawaban yang sangat bagus. Saya juga ingin menunjukkan sumber daya yang terbukti dapat membantu dengan pertanyaan tentang pohon ekspresi:

Sana adalah adalah proyek CodePlex oleh Microsoft disebut Waktu Proses Bahasa Dinamis. Dokumentasinya mencakup dokumen berjudul,"Expression Trees v2 Spec", yaitu: Spesifikasi pohon ekspresi LINQ di .NET 4.

Pembaruan: CodePlex sudah tidak berfungsi. The Trees Ekspresi v2 Spec (PDF) telah pindah ke GitHub .

Misalnya, dikatakan tentang Expression.Quote:

4.4.42 Kutipan

Gunakan Kutipan di UnaryExpressions untuk mewakili ekspresi yang memiliki nilai "konstan" dari jenis Expression. Tidak seperti node Constant, node Quote secara khusus menangani node ParameterExpression yang berisi. Jika node ParameterExpression yang terkandung mendeklarasikan lokal yang akan ditutup dalam ekspresi yang dihasilkan, maka Kutipan menggantikan ParameterExpression di lokasi referensinya. Pada waktu proses ketika node Quote dievaluasi, itu menggantikan referensi variabel closure untuk node referensi ParameterExpression, dan kemudian mengembalikan ekspresi yang dikutip. […] (Hlm. 63–64)

stakx mendukung GoFundMonica
sumber
1
Jawaban yang sangat bagus dari jenis ajar-orang-ke-ikan. Saya hanya ingin menambahkan bahwa dokumentasi telah dipindahkan dan sekarang tersedia di docs.microsoft.com/en-us/dotnet/framework/… . Dokumen yang dikutip, khususnya, ada di GitHub: github.com/IronLanguages/dlr/tree/master/Docs
relative_random
3

Setelah ini jawaban yang sangat bagus, jelas apa semantiknya. Tidak begitu jelas mengapa mereka dirancang seperti itu, pertimbangkan:

Expression.Lambda(Expression.Add(ps, pt));

Ketika lambda ini dikompilasi dan dipanggil, itu mengevaluasi ekspresi dalam dan mengembalikan hasilnya. Ekspresi dalam di sini adalah tambahan, sehingga ps + pt dievaluasi dan hasilnya dikembalikan. Mengikuti logika ini, ekspresi berikut:

Expression.Lambda(
    Expression.Lambda(
              Expression.Add(ps, pt),
            pt), ps);

harus mengembalikan referensi metode terkompilasi lambda bagian dalam ketika lambda luar dipanggil (karena kita mengatakan bahwa lambda mengompilasi ke referensi metode). Jadi mengapa kita membutuhkan Penawaran ?! Untuk membedakan kasus ketika referensi metode dikembalikan vs. hasil pemanggilan referensi tersebut.

Secara khusus:

let f = Func<...>
return f; vs. return f(...);

Karena beberapa alasan. Desainer .Net memilih Expression.Quote (f) untuk kasus pertama dan f polos untuk yang kedua. Menurut pandangan saya ini menyebabkan banyak kebingungan, karena dalam kebanyakan bahasa pemrograman mengembalikan nilai secara langsung (tidak perlu Kutipan atau operasi lainnya), tetapi pemanggilan memang memerlukan tulisan tambahan (tanda kurung + argumen), yang diterjemahkan menjadi beberapa jenis panggil di tingkat MSIL. Desainer. Net membuatnya kebalikan dari pohon ekspresi. Akan menarik untuk mengetahui alasannya.

Konstantin Triger
sumber
0

Saya percaya itu lebih seperti yang diberikan:

Expression<Func<Func<int>>> f = () => () => 2;

Pohon Anda adalah Expression.Lambda(Expression.Lambda)dan fmewakili Pohon Ekspresi untuk lambda yang mengembalikan Func<int>yang kembali 2.

Tetapi jika yang Anda inginkan adalah lambda yang mengembalikan Pohon Ekspresi untuk lambda yang kembali 2, maka Anda memerlukan:

Expression<Func<Expression<Func<int>>>> f = () => () => 2;

Dan sekarang pohon Anda adalah Expression.Lambda(Expression.Quote(Expression.Lambda))dan fmewakili Pohon Ekspresi untuk lambda yang mengembalikan Expression<Func<int>>itu adalah Pohon Ekspresi untuk Func<int>yang kembali 2.

NetMage
sumber
-2

Saya pikir intinya di sini adalah ekspresi pohon. Ekspresi konstan yang berisi delegasi sebenarnya hanya berisi objek yang kebetulan merupakan delegasi. Ini kurang ekspresif dibandingkan perincian langsung ke ekspresi uner dan biner.

Joe Wood
sumber
Apakah itu? Ekspresif apa yang ditambahkannya, tepatnya? Apa yang bisa Anda "ekspresikan" dengan UnaryExpression itu (yang juga merupakan jenis ekspresi aneh untuk digunakan) yang tidak dapat Anda ekspresikan dengan ConstantExpression?
Timwi