Mengapa Anda menggunakan Expression <Func <T>> daripada Func <T>?

949

Saya memahami lambdas dan Funcdan Actiondelegasi. Tapi ekspresi membuatku bingung.

Dalam situasi apa Anda akan menggunakan yang lama Expression<Func<T>>dan tidak biasa Func<T>?

Richard Nagle
sumber
14
Fungsi <> akan dikonversi menjadi metode pada level c # compiler, Ekspresi <Fungsi <>> akan dieksekusi pada level MSIL setelah mengkompilasi kode secara langsung, itulah sebabnya lebih cepat
Waleed AK
1
selain jawaban, spesifikasi bahasa csharp "4.6 jenis pohon ekspresi" sangat membantu untuk referensi silang
djeikyb

Jawaban:

1133

Saat Anda ingin memperlakukan ekspresi lambda sebagai pohon ekspresi dan lihat ke dalamnya alih-alih menjalankannya. Sebagai contoh, LINQ ke SQL mendapatkan ekspresi dan mengubahnya ke pernyataan SQL yang setara dan mengirimkannya ke server (daripada mengeksekusi lambda).

Secara konseptual, Expression<Func<T>>sama sekali berbeda dari Func<T>. Func<T>menunjukkan suatu delegateyang cukup banyak penunjuk ke metode dan Expression<Func<T>>menunjukkan struktur data pohon untuk ekspresi lambda. Struktur pohon ini menggambarkan apa yang dilakukan ekspresi lambda daripada melakukan hal yang sebenarnya. Ini pada dasarnya menyimpan data tentang komposisi ekspresi, variabel, pemanggilan metode, ... (misalnya ia menyimpan informasi seperti lambda ini adalah beberapa konstanta + beberapa parameter). Anda dapat menggunakan deskripsi ini untuk mengubahnya menjadi metode aktual (dengan Expression.Compile) atau melakukan hal-hal lain (seperti contoh LINQ ke SQL) dengannya. Tindakan memperlakukan lambda sebagai metode anonim dan pohon ekspresi adalah murni waktu kompilasi.

Func<int> myFunc = () => 10; // similar to: int myAnonMethod() { return 10; }

akan secara efektif mengkompilasi ke metode IL yang tidak mendapatkan apa pun dan mengembalikan 10.

Expression<Func<int>> myExpression = () => 10;

akan dikonversi ke struktur data yang menggambarkan ekspresi yang tidak memiliki parameter dan mengembalikan nilai 10:

Ekspresi vs Fungsi gambar yang lebih besar

Walaupun keduanya terlihat sama pada waktu kompilasi, apa yang dihasilkan oleh kompiler sama sekali berbeda .

Mehrdad Afshari
sumber
96
Jadi, dengan kata lain, a Expressionberisi informasi meta tentang delegasi tertentu.
bertl
40
@bertl Sebenarnya, tidak. Delegasi tidak terlibat sama sekali. Alasan mengapa ada asosiasi sama sekali dengan delegasi adalah bahwa Anda dapat mengkompilasi ekspresi ke delegasi - atau lebih tepatnya, kompilasi ke suatu metode dan mendapatkan delegasi ke metode itu sebagai nilai kembali. Tapi pohon ekspresi itu sendiri hanyalah data. Delegasi tidak ada saat Anda menggunakannya, Expression<Func<...>>bukan hanya Func<...>.
Luaan
5
@Kyle Delaney (isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }ekspresi seperti itu adalah ExpressionTree, cabang dibuat untuk pernyataan-If.
Matteo Marciano - MSCP
3
@bertl Delegate adalah apa yang dilihat CPU (kode yang dapat dieksekusi dari satu arsitektur), Ekspresi adalah apa yang dilihat oleh kompiler (hanya format lain dari kode sumber, tetapi masih kode sumber).
codewarrior
5
@bertl: Mungkin diringkas lebih akurat dengan mengatakan bahwa ekspresi adalah untuk func apa stringbuilder adalah string. Ini bukan string / func, tetapi berisi data yang dibutuhkan untuk membuatnya ketika diminta untuk melakukannya.
Flater
337

Saya menambahkan jawaban untuk noobs karena jawaban ini tampak di atas kepala saya, sampai saya menyadari betapa sederhananya itu. Kadang-kadang harapan Anda bahwa itu rumit yang membuat Anda tidak dapat 'membungkus kepala Anda'.

Saya tidak perlu memahami perbedaannya sampai saya menemukan 'bug' yang benar-benar mengganggu yang mencoba menggunakan LINQ-to-SQL secara umum:

public IEnumerable<T> Get(Func<T, bool> conditionLambda){
  using(var db = new DbContext()){
    return db.Set<T>.Where(conditionLambda);
  }
}

Ini bekerja dengan baik sampai saya mulai mendapatkan OutofMemoryExceptions pada kumpulan data yang lebih besar. Menetapkan breakpoint di dalam lambda membuat saya menyadari bahwa itu berulang melalui setiap baris di meja saya satu-per-satu mencari korek api dengan kondisi lambda saya. Ini mengejutkan saya untuk sementara waktu, karena mengapa itu memperlakukan tabel data saya sebagai IEnumerable raksasa daripada melakukan LINQ-to-SQL seperti yang seharusnya? Itu juga melakukan hal yang sama persis di rekan LINQ-to-MongoDb saya.

Cara mengatasinya adalah hanya untuk mengubah Func<T, bool>ke dalam Expression<Func<T, bool>>, jadi saya googled mengapa membutuhkan Expressionbukan Func, berakhir di sini.

Ekspresi hanya mengubah delegasi menjadi data tentang dirinya sendiri. Jadi a => a + 1menjadi sesuatu seperti "Di sisi kiri ada int a. Di sisi kanan Anda menambahkan 1 ke sana." Itu dia. Kamu bisa pulang sekarang. Ini jelas lebih terstruktur dari itu, tapi itu pada dasarnya semua pohon ekspresi sebenarnya - tidak ada yang membungkus kepala Anda.

Memahami itu, menjadi jelas mengapa LINQ-to-SQL membutuhkan Expression, dan Functidak memadai. Functidak membawa dengannya cara untuk masuk ke dirinya sendiri, untuk melihat seluk-beluk bagaimana menerjemahkannya ke dalam SQL / MongoDb / permintaan lainnya. Anda tidak dapat melihat apakah itu melakukan penambahan atau penggandaan atau pengurangan. Yang bisa Anda lakukan adalah menjalankannya. Expression, di sisi lain, memungkinkan Anda untuk melihat ke dalam delegasi dan melihat semua yang ingin dilakukan. Ini memberdayakan Anda untuk menerjemahkan delegasi ke dalam apa pun yang Anda inginkan, seperti kueri SQL. Functidak berfungsi karena DbContext saya buta terhadap isi ekspresi lambda. Karena itu, tidak bisa mengubah ekspresi lambda menjadi SQL; Namun, ia melakukan hal terbaik berikutnya dan mengulanginya dengan syarat melalui setiap baris di meja saya.

Sunting: menguraikan tentang kalimat terakhir saya atas permintaan John Peter:

IQueryable memperluas IEnumerable, jadi metode IEnumerable seperti Where()mendapatkan kelebihan beban yang menerima Expression. Ketika Anda lulus suatu Expressionuntuk itu, Anda menyimpan IQueryable sebagai hasilnya, tetapi ketika Anda lulus Func, Anda jatuh kembali pada basis IEnumerable dan Anda akan mendapatkan IEnumerable sebagai hasilnya. Dengan kata lain, tanpa memperhatikan Anda telah mengubah dataset Anda menjadi daftar untuk diiterasi sebagai lawan dari sesuatu untuk ditanyakan. Sulit untuk melihat perbedaan sampai Anda benar-benar melihat di bawah kap di tanda tangan.

Chad Hedgcock
sumber
2
Chad; Tolong jelaskan komentar ini sedikit lebih banyak: "Func tidak berfungsi karena DbContext saya buta terhadap apa yang sebenarnya ada dalam ekspresi lambda untuk mengubahnya menjadi SQL, jadi ia melakukan hal terbaik berikutnya dan mengulangi kondisi tersebut melalui setiap baris di meja saya . "
John Peters
2
>> Func ... Yang bisa Anda lakukan adalah menjalankannya. Itu tidak sepenuhnya benar, tetapi saya pikir itulah titik yang harus ditekankan. Fungsi / Tindakan harus dijalankan, Ekspresi harus dianalisis (sebelum berjalan atau bahkan bukannya berjalan).
Konstantin
@Chad Apakah masalahnya di sini adalah ?: db. Set <T> menanyakan semua tabel database, dan setelah itu, karena .Where (conditionLambda) menggunakan metode ekstensi Where (IEnumerable), yang menghitung seluruh tabel di memori . Saya pikir Anda mendapatkan OutOfMemoryException karena, kode ini mencoba memuat seluruh tabel ke memori (dan tentu saja menciptakan objek). Apakah saya benar? Terima kasih :)
Bence Végert
104

Pertimbangan yang sangat penting dalam pilihan Ekspresi vs Func adalah bahwa penyedia IQueryable seperti LINQ ke Entitas dapat 'mencerna' apa yang Anda berikan dalam Ekspresi, tetapi akan mengabaikan apa yang Anda lewati dalam Func. Saya memiliki dua posting blog tentang hal ini:

Lebih lanjut tentang Ekspresi vs Fungsi dengan Kerangka Entitas dan Jatuh Cinta dengan LINQ - Bagian 7: Ekspresi dan Fungsi (bagian terakhir)

LSpencer777
sumber
+ l untuk penjelasan. Namun saya mendapatkan 'Jenis simpul ekspresi LINQ' Invoke 'tidak didukung di LINQ untuk Entitas.' dan harus menggunakan ForEach setelah mengambil hasilnya.
tymtam
77

Saya ingin menambahkan beberapa catatan tentang perbedaan antara Func<T>dan Expression<Func<T>>:

  • Func<T> hanyalah MulticastDelegate sekolah tua yang normal;
  • Expression<Func<T>> adalah representasi ekspresi lambda dalam bentuk pohon ekspresi;
  • pohon ekspresi dapat dibangun melalui sintaks ekspresi lambda atau melalui sintaksis API;
  • pohon ekspresi dapat dikompilasi ke delegasi Func<T>;
  • konversi terbalik secara teori dimungkinkan, tetapi ini semacam penguraian, tidak ada fungsi bawaan untuk itu karena ini bukan proses yang mudah;
  • pohon ekspresi dapat diamati / diterjemahkan / dimodifikasi melalui ExpressionVisitor;
  • metode ekstensi untuk IEnumerable beroperasi dengan Func<T>;
  • metode ekstensi untuk beroperasi dengan IQueryable Expression<Func<T>>.

Ada artikel yang menjelaskan detail dengan contoh kode:
LINQ: Func <T> vs Expression <Func <T>> .

Semoga bermanfaat.

Olexander Ivanitskyi
sumber
Daftar yang bagus, satu catatan kecil adalah Anda menyebutkan bahwa konversi terbalik dimungkinkan, namun sebaliknya tidak. Beberapa metadata hilang selama proses konversi. Namun Anda bisa mendekompilasinya ke pohon Ekspresi yang menghasilkan hasil yang sama ketika dikompilasi lagi.
Aidiakapi
76

Ada penjelasan yang lebih filosofis tentang hal itu dari buku Krzysztof Cwalina ( Kerangka Desain Pedoman: Konvensi, Idiom, dan Pola untuk Reusable .NET Libraries );

Rico Mariani

Edit untuk versi non-gambar:

Sering kali Anda akan menginginkan Fungsi atau Aksi jika semua yang perlu terjadi adalah menjalankan beberapa kode. Anda perlu Ekspresi ketika kode perlu dianalisis, diserialisasi, atau dioptimalkan sebelum dijalankan. Ekspresi adalah untuk memikirkan kode, Fungsi / Aksi adalah untuk menjalankannya.

Oğuzhan Soykan
sumber
10
Baik. yaitu. Anda perlu berekspresi ketika Anda mengharapkan Fungsi Anda akan dikonversi menjadi semacam permintaan. Yaitu. Anda harus database.data.Where(i => i.Id > 0)dieksekusi sebagai SELECT FROM [data] WHERE [id] > 0. Jika Anda hanya lulus dalam Func, Anda telah menempatkan penutup mata pada driver Anda dan semua itu dapat dilakukan adalah SELECT *dan kemudian setelah itu dimuat semua data ke dalam memori, iterate melalui masing-masing dan menyaring segala sesuatu dengan id> 0. Wrapping Anda Funcdi Expressionmemberdayakan driver untuk menganalisis Funcdan mengubahnya menjadi permintaan Sql / MongoDb / lainnya.
Chad Hedgcock
Jadi ketika saya merencanakan untuk Liburan, saya akan menggunakan Expressiontetapi ketika saya sedang berlibur akan Func/Action;)
GoldBishop
1
@ChadHedgcock Ini adalah karya terakhir yang saya butuhkan. Terima kasih. Saya telah melihat ini untuk sementara waktu, dan komentar Anda di sini membuat semua klik penelitian.
johnny
37

LINQ adalah contoh kanonik (misalnya, berbicara ke database), tetapi sebenarnya, setiap kali Anda lebih peduli untuk mengungkapkan apa yang harus dilakukan, daripada benar-benar melakukannya. Sebagai contoh, saya menggunakan pendekatan ini di tumpukan RPC protobuf-net (untuk menghindari pembuatan kode dll) - jadi Anda memanggil metode dengan:

string result = client.Invoke(svc => svc.SomeMethod(arg1, arg2, ...));

Ini mendekonstruksi pohon ekspresi untuk menyelesaikan SomeMethod(dan nilai setiap argumen), melakukan panggilan RPC, memperbarui setiap ref/ outargumen, dan mengembalikan hasilnya dari panggilan jarak jauh. Ini hanya mungkin melalui pohon ekspresi. Saya membahas hal ini lebih lanjut di sini .

Contoh lain adalah ketika Anda membangun pohon ekspresi secara manual untuk tujuan mengkompilasi ke lambda, seperti yang dilakukan oleh kode operator generik .

Marc Gravell
sumber
20

Anda akan menggunakan ekspresi ketika Anda ingin memperlakukan fungsi Anda sebagai data dan bukan sebagai kode. Anda dapat melakukan ini jika Anda ingin memanipulasi kode (sebagai data). Sebagian besar waktu jika Anda tidak melihat kebutuhan untuk ekspresi maka Anda mungkin tidak perlu menggunakannya.

Andrew Hare
sumber
19

Alasan utamanya adalah ketika Anda tidak ingin menjalankan kode secara langsung, tetapi ingin memeriksanya. Ini bisa karena sejumlah alasan:

  • Memetakan kode ke lingkungan yang berbeda (mis. Kode C # ke SQL di Entity Framework)
  • Mengganti bagian-bagian kode dalam runtime (pemrograman dinamis atau bahkan teknik KERING polos)
  • Validasi kode (sangat berguna saat meniru skrip atau saat melakukan analisis)
  • Serialisasi - ekspresi dapat diserialisasi dengan mudah dan aman, delegasi tidak bisa
  • Keamanan yang sangat diketik pada hal-hal yang pada dasarnya tidak diketik dengan kuat, dan mengeksploitasi pemeriksaan kompiler meskipun Anda melakukan panggilan dinamis dalam runtime (ASP.NET MVC 5 dengan Razor adalah contoh yang bagus)
Luaan
sumber
dapatkah Anda menjelaskan lebih lanjut tentang no.5
uowzd01
@ uowzd01 Lihat saja Razor - ia menggunakan pendekatan ini secara luas.
Luaan
@Luaan Saya mencari serialisasi ekspresi tetapi tidak dapat menemukan apa pun tanpa penggunaan pihak ketiga yang terbatas. Apakah .Net 4.5 mendukung serialisasi pohon ekspresi?
vabii
@vabii Bukan yang saya tahu - dan itu tidak benar-benar ide yang baik untuk kasus umum. Maksud saya adalah lebih banyak tentang Anda dapat menulis serialisasi yang cukup sederhana untuk kasus-kasus spesifik yang ingin Anda dukung, terhadap antarmuka yang dirancang sebelumnya - saya telah melakukan hal itu beberapa kali. Dalam kasus umum, sebuah Expressionbisa saja mustahil untuk diserialisasi sebagai delegasi, karena ekspresi apa pun dapat berisi permohonan dari delegasi / metode referensi sewenang-wenang. "Mudah" itu relatif, tentu saja.
Luaan
15

Saya belum melihat jawaban apa pun yang menyebutkan kinerja. Melewati Func<>s ke Where()atau Count()buruk. Sangat buruk. Jika Anda menggunakan a Func<>maka itu memanggil hal- IEnumerablehal LINQ bukan IQueryable, yang berarti bahwa seluruh tabel ditarik dan kemudian disaring. Expression<Func<>>secara signifikan lebih cepat, terutama jika Anda meminta database yang tinggal di server lain.

mhenry1384
sumber
Apakah ini berlaku untuk permintaan dalam memori juga?
stt106
@ stt106 Mungkin tidak.
mhenry1384
Ini hanya benar jika Anda menyebutkan daftar. Jika Anda menggunakan GetEnumerator atau foreach Anda tidak akan memuat ienumerable sepenuhnya ke dalam memori.
nelsontruran
1
@ stt106 Ketika diteruskan ke klausa .Where () dari Daftar <>, Ekspresi <Func <>> mendapat .Compile () memanggilnya, jadi Func <> hampir pasti lebih cepat. Lihat Referenceource.microsoft.com/#System.Core/System/Linq/…
NStuke