ทำไมคุณถึงใช้ Expression <Func <T>> แทนที่จะเป็น Func <T>


949

ฉันเข้าใจ lambdas FuncและActionผู้ได้รับมอบหมาย แต่การแสดงออกตอฉัน

คุณจะใช้Expression<Func<T>>แบบเก่ามากกว่าแบบธรรมดาในสถานการณ์Func<T>ใด


14
Func <> จะถูกแปลงเป็นวิธีการในระดับ c # คอมไพเลอร์ Expression <Func <>> จะถูกดำเนินการในระดับ MSIL หลังจากรวบรวมรหัสโดยตรงนั่นคือเหตุผลที่เร็วขึ้น
Waleed AK

1
นอกเหนือจากคำตอบแล้วข้อมูลจำเพาะภาษา csharp "4.6 tree tree types" มีประโยชน์ในการอ้างอิงข้าม
djeikyb

คำตอบ:


1133

เมื่อคุณต้องการรักษาแลมบ์ดานิพจน์เป็นต้นไม้นิพจน์และดูข้างในพวกเขาแทนการดำเนินการเหล่านั้น ตัวอย่างเช่น LINQ ไปยัง SQL รับนิพจน์และแปลงเป็นคำสั่ง SQL ที่เทียบเท่าและส่งไปยังเซิร์ฟเวอร์ (แทนที่จะเรียกใช้แลมบ์ดา)

แนวคิดExpression<Func<T>>คือสมบูรณ์แตกต่างFunc<T>จาก Func<T>หมายถึง a delegateซึ่งเป็นตัวชี้ไปยังเมธอดและExpression<Func<T>>แสดงโครงสร้างข้อมูลทรีสำหรับนิพจน์แลมบ์ดา โครงสร้างต้นไม้นี้อธิบายถึงสิ่งที่แลมบ์ดาแสดงออกแทนที่จะทำสิ่งที่เกิดขึ้นจริง มันเก็บข้อมูลเกี่ยวกับองค์ประกอบของการแสดงออกตัวแปรการเรียกใช้เมธอด ... (ตัวอย่างเช่นมันเก็บข้อมูลเช่นแลมบ์ดานี้มีค่าคงที่ + พารามิเตอร์บางตัว) คุณสามารถใช้คำอธิบายนี้เพื่อแปลงเป็นวิธีการจริง (พร้อมExpression.Compile) หรือทำสิ่งอื่น ๆ (เช่นตัวอย่าง LINQ เป็น SQL) ด้วย การกระทำของการรักษาลูกแกะเป็นวิธีการไม่ระบุชื่อและต้นไม้แสดงออกเป็นเพียงเวลารวบรวม

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

จะรวบรวมอย่างมีประสิทธิภาพกับวิธีการ IL ที่ไม่ได้อะไรเลยและส่งคืน 10

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

จะถูกแปลงเป็นโครงสร้างข้อมูลที่อธิบายนิพจน์ที่ไม่ได้รับพารามิเตอร์และส่งคืนค่า 10:

นิพจน์และ Func ภาพใหญ่ขึ้น

ในขณะที่พวกเขาทั้งสองมีลักษณะเดียวกันที่รวบรวมเวลาสิ่งที่คอมไพเลอร์สร้างเป็นที่แตกต่างกันโดยสิ้นเชิง


96
ดังนั้นในคำอื่น ๆ ที่Expressionมีข้อมูลเมตาเกี่ยวกับผู้รับมอบสิทธิ์ที่แน่นอน
bertl

40
@bertl ที่จริงแล้วไม่มี ผู้รับมอบสิทธิ์ไม่เกี่ยวข้องเลย เหตุผลที่มีการเชื่อมโยงใด ๆ เลยกับผู้รับมอบสิทธิ์คือคุณสามารถรวบรวมนิพจน์ไปยังผู้รับมอบสิทธิ์ - หรือเพื่อให้แม่นยำยิ่งขึ้นรวบรวมไปยังวิธีการและรับมอบหมายให้วิธีนั้นเป็นค่าตอบแทน แต่ทรีนิพจน์เองนั้นเป็นเพียงข้อมูล ตัวแทนไม่ได้อยู่เมื่อคุณใช้แทนเพียงExpression<Func<...>> Func<...>
Luaan

5
@Kyle Delaney (isAnExample) => { if(isAnExample) ok(); else expandAnswer(); }นิพจน์ดังกล่าวเป็น ExpressionTree สาขาถูกสร้างขึ้นสำหรับคำสั่ง If
Matteo Marciano - MSCP

3
@bertl Delegate คือสิ่งที่ CPU เห็น (โค้ดที่รันได้ของสถาปัตยกรรมหนึ่ง), Expression คือสิ่งที่คอมไพเลอร์เห็น (เป็นเพียงรูปแบบของซอร์สโค้ดอื่น แต่ยังเป็นซอร์สโค้ด)
codewarrior

5
@bertl: มันอาจจะสรุปได้อย่างแม่นยำมากขึ้นโดยบอกว่าการแสดงออกถึง func สิ่งที่ผู้สร้างสตริงเป็นสตริง มันไม่ใช่สตริง / func แต่มีข้อมูลที่จำเป็นในการสร้างเมื่อขอให้ทำ
Flater

337

ฉันกำลังเพิ่มคำตอบสำหรับ noobs เพราะคำตอบเหล่านี้ปรากฏบนหัวของฉันจนกว่าฉันจะรู้ว่ามันง่ายแค่ไหน บางครั้งมันเป็นความคาดหวังของคุณว่ามีความซับซ้อนที่ทำให้คุณไม่สามารถ 'ห่อหัวของคุณไปรอบ ๆ มัน'

ฉันไม่จำเป็นต้องเข้าใจความแตกต่างจนกว่าฉันจะเข้าสู่ 'ข้อผิดพลาด' ที่น่ารำคาญจริงๆที่พยายามใช้ LINQ-to-SQL โดยทั่วไป:

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

สิ่งนี้ทำงานได้ดีจนกระทั่งฉันเริ่มรับ OutofMemoryExceptions บนชุดข้อมูลขนาดใหญ่ การตั้งค่าเบรกพอยต์ภายในแลมบ์ดาทำให้ฉันรู้ว่ามันซ้ำแล้วซ้ำอีกในแต่ละแถวในตารางของฉันทีละคนมองหาแมตช์กับสภาพแลมบ์ดาของฉัน สิ่งนี้ทำให้ฉันงงอยู่ครู่หนึ่งเพราะเหตุใด heck จึงปฏิบัติกับตารางข้อมูลของฉันในฐานะ IEnumerable ขนาดยักษ์แทนที่จะใช้ LINQ-to-SQL อย่างที่ควรจะเป็น มันก็ทำสิ่งเดียวกันใน LINQ-to-MongoDb ของฉันด้วย

การแก้ไขเป็นเพียงการเปลี่ยนFunc<T, bool>เป็นExpression<Func<T, bool>>ดังนั้นฉันจึง googled ทำไมมันต้องการExpressionแทนที่จะFuncจบลงที่นี่

การแสดงออกเพียงเปลี่ยนผู้แทนเป็นข้อมูลเกี่ยวกับตัวเอง ดังนั้นa => a + 1จะกลายเป็นสิ่งที่ชอบ "ที่ด้านซ้ายมีการint a. ทางด้านขวาคุณเพิ่ม 1 ถึงมัน." แค่นั้นแหละ. คุณสามารถกลับบ้านได้แล้ว เห็นได้ชัดว่ามีโครงสร้างมากกว่านั้น แต่ที่จริงแล้วต้นไม้นิพจน์เป็นจริง - ไม่มีอะไรห่อหัวของคุณ

เมื่อเข้าใจว่าจะเห็นได้ชัดว่าทำไม LINQ-to-SQL จึงต้องมีExpressionและFuncไม่เพียงพอ Funcไม่ได้มีวิธีในการเข้าไปในตัวเองเพื่อดู nitty-gritty ของวิธีการแปลเป็น SQL / MongoDb / แบบสอบถามอื่น ๆ คุณไม่สามารถดูได้ว่ามันกำลังทำการเพิ่มหรือการคูณหรือการลบ สิ่งที่คุณทำได้ก็คือเรียกใช้ Expressionในทางกลับกันอนุญาตให้คุณมองเข้าไปข้างในผู้รับมอบสิทธิ์และดูทุกสิ่งที่ต้องการ สิ่งนี้ช่วยให้คุณสามารถแปลผู้รับมอบสิทธิ์เป็นสิ่งที่คุณต้องการเช่นแบบสอบถาม SQLFuncไม่ทำงานเพราะ DbContext ของฉันตาบอดกับเนื้อหาของการแสดงออกแลมบ์ดา ด้วยเหตุนี้จึงไม่สามารถแปลงแลมบ์ดาให้เป็น SQL ได้ อย่างไรก็ตามมันทำสิ่งที่ดีที่สุดถัดไปและทำซ้ำเงื่อนไขนั้นในแต่ละแถวในตารางของฉัน

แก้ไข: อธิบายประโยคสุดท้ายของฉันตามคำร้องขอของ John Peter:

IQueryable ขยาย IEnumerable ดังนั้นวิธีการ IEnumerable เหมือนได้รับการยอมรับว่าเกินWhere() Expressionเมื่อคุณส่งผ่านExpressionไปยังที่คุณเก็บ IQueryable เป็นผล แต่เมื่อคุณผ่านFuncคุณจะล้มลงบนฐาน IEnumerable และคุณจะได้รับ IEnumerable เป็นผล กล่าวอีกนัยหนึ่งโดยไม่สังเกตว่าคุณเปลี่ยนชุดข้อมูลของคุณไปเป็นรายการให้ทำซ้ำซึ่งตรงข้ามกับบางสิ่งที่จะสืบค้น มันยากที่จะสังเกตเห็นความแตกต่างจนกว่าคุณจะเห็นภายใต้ประทุนที่ลายเซ็น


2
ชาด; โปรดอธิบายความคิดเห็นนี้อีกเล็กน้อย: "Func ไม่ทำงานเพราะ DbContext ของฉันตาบอดกับสิ่งที่เกิดขึ้นจริงในนิพจน์แลมบ์ดาเพื่อเปลี่ยนเป็น SQL ดังนั้นจึงทำสิ่งที่ดีที่สุดถัดไปและทำซ้ำเงื่อนไขนั้นผ่านแต่ละแถวในตารางของฉัน ."
John Peters

2
>> Func ... สิ่งที่คุณทำได้ก็แค่เปิดมัน มันไม่เป็นความจริง แต่ฉันคิดว่านั่นเป็นประเด็นที่ควรเน้น Funcs / Actions จะถูกเรียกใช้นิพจน์จะถูกวิเคราะห์ (ก่อนเรียกใช้หรือแม้กระทั่งแทนที่จะเรียกใช้)
Konstantin

@Chad ปัญหาอยู่ที่นี่ใช่หรือไม่: db.Set <T> สอบถามตารางฐานข้อมูลทั้งหมดและหลังจากนั้นเนื่องจาก. Where (conditionLambda) ใช้วิธีการขยาย Where (IEnumerable) ซึ่งระบุในตารางทั้งหมดในหน่วยความจำ . ฉันคิดว่าคุณได้รับ OutOfMemoryException เพราะรหัสนี้พยายามโหลดทั้งตารางไปยังหน่วยความจำ (และแน่นอนว่าสร้างวัตถุ) ฉันถูกไหม? ขอบคุณ :)
Bence Végert

104

การพิจารณาที่สำคัญอย่างยิ่งในการเลือก Expression vs Func คือผู้ให้บริการ IQueryable เช่น LINQ to Entities สามารถ 'ย่อย' สิ่งที่คุณผ่านใน Expression แต่จะเพิกเฉยต่อสิ่งที่คุณส่งผ่าน Func ฉันมีบทความในบล็อกสองเรื่อง:

เพิ่มเติมเกี่ยวกับ Expression vs Func พร้อม Entity Frameworkและ หลงรัก LINQ - ตอนที่ 7: การแสดงออกและ Funcs (ส่วนสุดท้าย)


+ l สำหรับคำอธิบาย อย่างไรก็ตามฉันได้รับ 'ประเภทโหนด LINQ นิพจน์' เรียกใช้ 'ไม่รองรับใน LINQ ไปยังเอนทิตี' และต้องใช้ ForEach หลังจากดึงผลลัพธ์
tymtam

77

ฉันต้องการเพิ่มบันทึกเกี่ยวกับความแตกต่างระหว่างFunc<T>และExpression<Func<T>>:

  • Func<T> เป็นเพียง MulticastDelegate โรงเรียนเก่า;
  • Expression<Func<T>> เป็นตัวแทนของแลมบ์ดาแสดงออกในรูปแบบของต้นไม้แสดงออก;
  • ต้นไม้แสดงออกสามารถสร้างได้ผ่านไวยากรณ์การแสดงออกแลมบ์ดาหรือผ่านไวยากรณ์ API;
  • ทรีนิพจน์สามารถถูกคอมไพล์ไปยังผู้รับมอบสิทธิ์ Func<T> ;
  • การแปลงผกผันเป็นไปได้ในทางทฤษฎี แต่เป็นวิธีการถอดรหัสไม่มีฟังก์ชั่นในตัวสำหรับสิ่งนั้นเนื่องจากมันไม่ได้เป็นกระบวนการที่ตรงไปตรงมา
  • ต้นไม้แสดงออกสามารถสังเกต / แปล / แก้ไขผ่านทาง ExpressionVisitor ;
  • วิธีการขยายสำหรับ IEnumerable ทำงานด้วย Func<T> ;
  • วิธีการขยายสำหรับ IQueryable Expression<Func<T>>ทำงานด้วย

มีบทความที่อธิบายรายละเอียดพร้อมตัวอย่างรหัส:
LINQ: Func <T> กับนิพจน์ <Func <T>>เทียบกับการแสดงออก

หวังว่ามันจะเป็นประโยชน์


รายการที่ดีโน้ตเล็ก ๆ น้อย ๆ คือคุณพูดถึงว่าการแปลงผกผันเป็นไปได้ แต่การผกผันที่แน่นอนไม่ได้ ข้อมูลเมตาบางส่วนหายไประหว่างกระบวนการแปลง อย่างไรก็ตามคุณสามารถคอมไพล์มันลงในทรีนิพจน์ที่สร้างผลลัพธ์เดียวกันเมื่อรวบรวมอีกครั้ง
Aidiakapi

76

มีคำอธิบายเพิ่มเติมเกี่ยวกับปรัชญาจากหนังสือของ Krzysztof Cwalina ( แนวทางการออกแบบกรอบการทำงาน: อนุสัญญาสำนวนและรูปแบบสำหรับไลบรารี. NET ที่ใช้ซ้ำได้ );

Rico Mariani

แก้ไขสำหรับเวอร์ชั่นที่ไม่ใช่รูปภาพ:

เวลาส่วนใหญ่คุณจะต้องการFuncหรือActionหากสิ่งที่ต้องเกิดขึ้นคือการเรียกใช้โค้ด คุณต้องการนิพจน์เมื่อต้องวิเคราะห์วิเคราะห์ลำดับหรือปรับให้เหมาะสมก่อนที่จะรัน นิพจน์มีไว้สำหรับคิดเกี่ยวกับโค้ดFunc / Actionใช้สำหรับเรียกใช้


10
ใส่กัน กล่าวคือ คุณต้องการนิพจน์เมื่อคุณคาดว่า Func ของคุณจะถูกแปลงเป็นแบบสอบถามบางประเภท กล่าวคือ คุณต้องการที่จะดำเนินการเป็นdatabase.data.Where(i => i.Id > 0) SELECT FROM [data] WHERE [id] > 0หากคุณเพียงแค่ผ่านใน Func คุณได้ใส่ blinders กับไดรเวอร์ของคุณและทั้งหมดก็สามารถทำได้คือSELECT *แล้วเมื่อมีการโหลดข้อมูลทั้งหมดที่เป็นหน่วยความจำย้ำผ่านแต่ละคนและกรองออกมาทุกอย่างที่มี ID> 0 ห่อของคุณFuncในการExpressionให้อำนาจ โปรแกรมควบคุมเพื่อวิเคราะห์Funcและเปลี่ยนเป็นคิวรี Sql / MongoDb / อื่น ๆ
ชาด Hedgcock

ดังนั้นเมื่อฉันวางแผนสำหรับวันหยุดพักผ่อนฉันจะใช้Expressionแต่เมื่อฉันอยู่ในช่วงวันหยุดมันจะเป็นFunc/Action;)
GoldBishop

1
@ChadHedgcock นี่เป็นชิ้นสุดท้ายที่ฉันต้องการ ขอบคุณ ฉันได้ดูสิ่งนี้มาระยะหนึ่งแล้วและความคิดเห็นของคุณที่นี่ทำให้ทุกการศึกษาคลิก
johnny

37

LINQ เป็นตัวอย่างที่ยอมรับได้ (ตัวอย่างเช่นการพูดคุยกับฐานข้อมูล) แต่ตามความเป็นจริงทุกครั้งที่คุณสนใจเพิ่มเติมเกี่ยวกับการแสดงสิ่งที่ต้องทำแทนที่จะทำตามจริง ตัวอย่างเช่นฉันใช้วิธีนี้ในกอง RPC ของ protobuf-net (เพื่อหลีกเลี่ยงการสร้างรหัส ฯลฯ ) - ดังนั้นคุณจึงเรียกวิธีด้วย:

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

สิ่งนี้แยกโครงสร้างต้นไม้นิพจน์เพื่อแก้ไขSomeMethod(และค่าของแต่ละอาร์กิวเมนต์) ทำการเรียก RPC อัพเดตใด ๆref / outargs และส่งคืนผลลัพธ์จากการเรียกระยะไกล สิ่งนี้สามารถทำได้ผ่านทรีนิพจน์ ฉันครอบคลุมสิ่งนี้เพิ่มเติมได้ที่นี่ที่นี่

อีกตัวอย่างหนึ่งคือเมื่อคุณสร้างต้นไม้นิพจน์ด้วยตนเองเพื่อจุดประสงค์ในการรวบรวมแลมบ์ดาดังที่ทำโดยรหัสตัวดำเนินการทั่วไป


20

คุณจะใช้นิพจน์เมื่อคุณต้องการใช้ฟังก์ชันเป็นข้อมูลไม่ใช่รหัส คุณสามารถทำได้ถ้าคุณต้องการจัดการรหัส (เป็นข้อมูล) เวลาส่วนใหญ่ถ้าคุณไม่เห็นความต้องการนิพจน์คุณอาจไม่จำเป็นต้องใช้นิพจน์


19

เหตุผลหลักคือเมื่อคุณไม่ต้องการรันโค้ดโดยตรง แต่ต้องการตรวจสอบมัน สิ่งนี้สามารถทำได้ด้วยเหตุผลหลายประการ:

  • การแมปรหัสกับสภาพแวดล้อมที่แตกต่างกัน (เช่นรหัส C # กับ SQL ใน Entity Framework)
  • การแทนที่ส่วนของรหัสในรันไทม์ (การเขียนโปรแกรมแบบไดนามิกหรือเทคนิค DRY ธรรมดา)
  • การตรวจสอบรหัส (มีประโยชน์มากเมื่อลอกเลียนแบบสคริปต์หรือเมื่อทำการวิเคราะห์)
  • การทำให้เป็นอนุกรม - นิพจน์สามารถทำให้เป็นอนุกรมได้อย่างง่ายดายและปลอดภัยผู้ได้รับมอบหมายไม่สามารถทำได้
  • ความปลอดภัยขั้นสูงสำหรับสิ่งที่ไม่ได้พิมพ์อย่างเด่นชัดและใช้ประโยชน์จากการตรวจสอบคอมไพเลอร์แม้ว่าคุณจะทำการโทรแบบไดนามิกในรันไทม์ (ASP.NET MVC 5 กับมีดโกนเป็นตัวอย่างที่ดี)

คุณช่วยอธิบายเพิ่มเติมอีกเล็กน้อยได้ที่หมายเลข 5
uowzd01

@ uowzd01 เพียงแค่ดูที่มีดโกน - มันใช้วิธีการนี้อย่างกว้างขวาง
Luaan

@Luaan ฉันกำลังมองหาการทำให้เป็นอนุกรมของนิพจน์ แต่ไม่สามารถหาอะไรโดยไม่ใช้บุคคลที่สาม จำกัด . Net 4.5 รองรับการแสดงออกเป็นลำดับต้นไม้หรือไม่
vabii

@vabii ไม่ใช่ที่ฉันรู้ - และมันจะไม่เป็นความคิดที่ดีสำหรับกรณีทั่วไป ประเด็นของฉันคือเกี่ยวกับคุณที่จะสามารถเขียนซีเรียลไลซ์เซชั่นได้ค่อนข้างง่ายสำหรับกรณีเฉพาะที่คุณต้องการให้การสนับสนุนกับอินเทอร์เฟซที่ออกแบบไว้ล่วงหน้า - ฉันทำไปแล้วสองสามครั้ง ในกรณีทั่วไปExpressionสามารถเป็นไปไม่ได้ที่จะทำให้เป็นอนุกรมในฐานะผู้รับมอบสิทธิ์เนื่องจากนิพจน์ใด ๆ สามารถมีการร้องขอของการอ้างอิงผู้แทน / วิธีการโดยพลการ "ง่าย" เป็นญาติแน่นอน
Luaan

15

ฉันยังไม่เห็นคำตอบใด ๆ ที่กล่าวถึงประสิทธิภาพ ผ่านFunc<> s เข้าไปWhere()หรือCount()ไม่ดี ไม่ดีจริง ถ้าคุณใช้ a Func<>ก็จะเรียกIEnumerableสิ่ง LINQ แทนIQueryableซึ่งหมายความว่าดึงตารางทั้งหมดแล้วกรอง Expression<Func<>>เร็วกว่ามากโดยเฉพาะถ้าคุณทำการสืบค้นฐานข้อมูลที่ใช้งานเซิร์ฟเวอร์อื่น


สิ่งนี้นำไปใช้กับแบบสอบถามในหน่วยความจำด้วยหรือไม่
stt106

@ stt106 อาจไม่
mhenry1384

นี่เป็นจริงเฉพาะถ้าคุณระบุรายการ ถ้าคุณใช้ GetEnumerator หรือ foreach คุณจะไม่โหลด ienumerable ลงในหน่วยความจำอย่างเต็มที่
nelsontruran

1
@ stt106 เมื่อส่งผ่านไปยัง. Where () ส่วนคำสั่งของ List <>, Expression <Func <>> ได้รับ .Compile () เรียกใช้มันดังนั้น Func <> จึงเกือบเร็วขึ้นอย่างแน่นอน ดูการอ้างอิง
source.microsoft.com/#System.Core/System/Linq/…
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.