Expression.Quote () ทำอะไร Expression.Constant () ไม่ได้แล้ว


98

หมายเหตุ: ฉันทราบถึงคำถามก่อนหน้านี้ " อะไรคือจุดประสงค์ของวิธี Expression.Quote ของ LINQ? แต่ถ้าคุณอ่านคุณจะเห็นว่ามันไม่ตอบคำถามของฉัน

ฉันเข้าใจจุดประสงค์ที่ระบุไว้Expression.Quote()คืออะไร อย่างไรก็ตามExpression.Constant()สามารถใช้เพื่อวัตถุประสงค์เดียวกันได้ (นอกเหนือจากวัตถุประสงค์ทั้งหมดที่Expression.Constant()ใช้สำหรับ) ดังนั้นฉันไม่เข้าใจว่าทำไมจึงExpression.Quote()จำเป็นต้องมี

เพื่อแสดงให้เห็นสิ่งนี้ฉันได้เขียนตัวอย่างสั้น ๆ ที่ซึ่งปกติจะใช้Quote(ดูบรรทัดที่มีเครื่องหมายอัศเจรีย์) แต่ฉันใช้Constantแทนและมันก็ทำงานได้ดีพอ ๆ กัน:

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);

ผลลัพธ์ของexpr.ToString()ทั้งสองเหมือนกันเช่นกัน (ไม่ว่าฉันจะใช้ConstantหรือQuote)

จากข้อสังเกตข้างต้นปรากฏว่าExpression.Quote()ซ้ำซ้อน คอมไพเลอร์ C # สามารถสร้างขึ้นเพื่อรวบรวมนิพจน์แลมบ์ดาที่ซ้อนกันลงในโครงสร้างนิพจน์ที่เกี่ยวข้องExpression.Constant()แทนExpression.Quote()และผู้ให้บริการแบบสอบถาม LINQ ใด ๆ ที่ต้องการประมวลผลแผนภูมินิพจน์เป็นภาษาแบบสอบถามอื่น ๆ (เช่น SQL) สามารถมองหา a ConstantExpressionwith type Expression<TDelegate>แทน ที่UnaryExpressionมีQuoteประเภทโหนดพิเศษและทุกอย่างจะเหมือนกัน

ฉันขาดอะไรไป? เหตุใดจึงมีExpression.Quote()การคิดค้นQuoteประเภทโหนดพิเศษและUnaryExpression

คำตอบ:


193

คำตอบสั้น ๆ :

ผู้ประกอบการอ้างเป็นผู้ประกอบการที่ก่อให้เกิดความหมายในตัวถูกดำเนินการปิดของมัน ค่าคงที่เป็นเพียงค่า

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

คำตอบยาว:

พิจารณาสิ่งต่อไปนี้:

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

แลมบ์ดาด้านนอกเป็นโรงงานสำหรับแอดเดอร์ที่ผูกไว้กับพารามิเตอร์ของแลมบ์ดาด้านนอก

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

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

        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));

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

สมมติว่าเราต้องการให้สถานะที่คอมไพล์ส่งคืนโครงสร้างนิพจน์ของการตกแต่งภายใน มีสองวิธีในการดำเนินการคือวิธีที่ง่ายและวิธีที่ยาก

วิธีที่ยากคือการพูดแทน

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

สิ่งที่เราหมายถึงคือ

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

จากนั้นสร้างแผนภูมินิพจน์สำหรับสิ่งนั้นโดยสร้างระเบียบนี้ :

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

blah blah blah รหัสสะท้อนหลายสิบบรรทัดเพื่อสร้างแลมด้า จุดประสงค์ของผู้ประกอบการอ้างคือการบอกคอมไพเลอร์ต้นไม้แสดงออกว่าเราต้องการแลมบ์ดาที่กำหนดจะถือว่าเป็นต้นไม้แสดงออกไม่เป็นฟังก์ชั่นโดยไม่ต้องสร้างอย่างชัดเจนแสดงออกรหัสรุ่นต้นไม้

วิธีง่ายๆคือ:

        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));

และแน่นอนถ้าคุณรวบรวมและเรียกใช้โค้ดนี้คุณจะได้รับคำตอบที่ถูกต้อง

โปรดสังเกตว่าเครื่องหมายคำพูดเป็นตัวดำเนินการที่ทำให้เกิดความหมายปิดบนแลมด้าภายในซึ่งใช้ตัวแปรด้านนอกซึ่งเป็นพารามิเตอร์ที่เป็นทางการของแลมด้าภายนอก

คำถามคือทำไมไม่กำจัด Quote และทำให้สิ่งนี้เป็นแบบเดียวกัน?

        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));

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

เนื่องจากไม่มีการกระตุ้นให้เกิดการปิดหากคุณทำเช่นนี้คุณจะได้รับ "ตัวแปร" ของประเภท "System.Int32" ไม่ได้กำหนด "ข้อยกเว้นในการเรียกใช้

(นอกเหนือจาก: ฉันเพิ่งตรวจสอบตัวสร้างโค้ดสำหรับการสร้างผู้รับมอบสิทธิ์จากต้นไม้นิพจน์ที่ยกมาและน่าเสียดายที่ความคิดเห็นที่ฉันใส่ลงในโค้ดในปี 2006 ยังคงอยู่ที่นั่น FYI พารามิเตอร์ด้านนอกที่ยกขึ้นจะถูกรวมเป็นค่าคงที่เมื่อยกมา แผนภูมินิพจน์ถูก reified เป็นผู้รับมอบสิทธิ์โดยคอมไพเลอร์รันไทม์มีเหตุผลที่ดีว่าทำไมฉันจึงเขียนโค้ดในแบบที่ฉันจำไม่ได้ในขณะนี้ แต่มันมีผลข้างเคียงที่น่ารังเกียจจากการแนะนำการปิดทับค่าของพารามิเตอร์ภายนอก แทนที่จะปิดตัวแปร. เห็นได้ชัดว่าทีมที่สืบทอดรหัสนั้นตัดสินใจที่จะไม่แก้ไขข้อบกพร่องนั้นดังนั้นหากคุณอาศัยการกลายพันธุ์ของพารามิเตอร์ภายนอกแบบปิดที่สังเกตได้ในแลมด้าภายในที่รวบรวมมาคุณจะต้องผิดหวัง อย่างไรก็ตามเนื่องจากเป็นการฝึกการเขียนโปรแกรมที่ค่อนข้างแย่สำหรับทั้ง (1) การกลายพันธุ์พารามิเตอร์ที่เป็นทางการและ (2) อาศัยการกลายพันธุ์ของตัวแปรภายนอกฉันขอแนะนำให้คุณเปลี่ยนโปรแกรมของคุณเพื่อไม่ใช้วิธีการเขียนโปรแกรมที่ไม่ดีทั้งสองนี้แทนที่จะเป็น กำลังรอการแก้ไขซึ่งดูเหมือนจะไม่เกิดขึ้น ขออภัยในความผิดพลาด)

ดังนั้นเพื่อตอบคำถามซ้ำ:

คอมไพเลอร์ C # สามารถสร้างขึ้นเพื่อรวบรวมนิพจน์แลมบ์ดาที่ซ้อนกันเป็นแผนภูมินิพจน์ที่เกี่ยวข้องกับ Expression.Constant () แทน Expression.Quote () และผู้ให้บริการแบบสอบถาม LINQ ใด ๆ ที่ต้องการประมวลผลแผนภูมินิพจน์เป็นภาษาแบบสอบถามอื่น ๆ (เช่น SQL ) สามารถมองหา ConstantExpression ที่มีประเภท Expression แทนที่จะเป็น UnaryExpression ที่มีประเภทโหนดใบเสนอราคาพิเศษและอย่างอื่นก็จะเหมือนกัน

คุณถูก. เราสามารถเข้ารหัสข้อมูลความหมายที่หมายถึง "ทำให้เกิดความหมายปิดกับค่านี้" โดยใช้ชนิดของนิพจน์คงที่เป็นแฟล็

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

แต่ทำไมจะเราทำสิ่งที่บ้า? ตัวดำเนินการใบเสนอราคาเป็นตัวดำเนินการที่ซับซ้อนอย่างไม่น่าเชื่อและควรใช้อย่างชัดเจนหากคุณจะใช้ คุณกำลังแนะนำว่าเพื่อเป็นการอธิบายอย่างชัดเจนเกี่ยวกับการไม่เพิ่มวิธีการโรงงานและประเภทโหนดพิเศษหนึ่งรายการจากจำนวนโหลที่มีอยู่แล้วเราจึงเพิ่มกรณีมุมที่แปลกประหลาดให้กับค่าคงที่เพื่อให้ค่าคงที่ในบางครั้งเป็นค่าคงที่เชิงตรรกะและบางครั้งก็ถูกเขียนใหม่ lambdas ที่มีความหมายปิด

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

และโปรดทราบว่าตอนนี้คุณกำลังวางภาระในการทำความเข้าใจ (นั่นคือการเข้าใจว่าค่าคงที่มีความหมายที่ซับซ้อนซึ่งหมายถึง "ค่าคงที่" ในกรณีเดียวและ "ทำให้เกิดความหมายปิด" ตามแฟล็กที่อยู่ในระบบประเภท ) กับทุกๆผู้ให้บริการที่วิเคราะห์ความหมายของแผนภูมินิพจน์ไม่ใช่เฉพาะกับผู้ให้บริการของ Microsoft ผู้ให้บริการบุคคลที่สามเหล่านี้จะเข้าใจผิดได้อย่างไร

"ใบเสนอราคา" โบกธงสีแดงขนาดใหญ่ที่มีข้อความว่า "เฮ้เพื่อนดูนี่สิฉันเป็นรูปแลมบ์ดาที่ซ้อนกันและฉันมีความหมายแปลกประหลาดถ้าฉันปิดตัวแปรภายนอก ในขณะที่ "ค่าคงที่" กำลังพูดว่า "ฉันไม่มีค่ามากไปกว่าค่าใช้จ่ายฉันตามที่คุณเห็นสมควร" เมื่อมีบางสิ่งที่ซับซ้อนและเป็นอันตรายเราต้องการทำให้มันโบกธงสีแดงไม่ใช่ซ่อนความจริงนั้นโดยการทำให้ผู้ใช้ขุดผ่านระบบประเภทเพื่อค้นหาว่าค่านี้เป็นค่าพิเศษหรือไม่

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


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

19

คำถามนี้ได้รับคำตอบที่ดีเยี่ยมแล้ว นอกจากนี้ฉันต้องการชี้ไปที่แหล่งข้อมูลที่สามารถพิสูจน์ได้ว่ามีประโยชน์สำหรับคำถามเกี่ยวกับต้นไม้นิพจน์:

ที่นั่น คือ เป็นโครงการ CodePlex โดย Microsoft เรียกว่า รันไทม์ภาษาไดนามิก. เอกสารประกอบรวมถึงเอกสารชื่อ"ต้นไม้นิพจน์ v2 Spec"ซึ่งตรงตามนั้น: ข้อกำหนดสำหรับแผนภูมินิพจน์ LINQ ใน. NET 4

อัปเดต: CodePlex ถูกยกเลิก Expression ต้นไม้ v2 Spec (PDF)ได้ย้ายไป GitHub

ตัวอย่างเช่นข้อความต่อไปนี้เกี่ยวกับExpression.Quote:

4.4.42 ใบเสนอราคา

ใช้ Quote ใน UnaryExpressions เพื่อแสดงนิพจน์ที่มีค่า "คงที่" ของประเภท Expression ซึ่งแตกต่างจากโหนดคงที่โหนดใบเสนอราคาจะจัดการกับโหนด ParameterExpression ที่มีอยู่เป็นพิเศษ หากโหนด ParameterExpression ที่มีอยู่ประกาศโลคัลที่จะปิดในนิพจน์ผลลัพธ์ Quote จะแทนที่ ParameterExpression ในตำแหน่งอ้างอิง ในขณะรันเมื่อมีการประเมินโหนดใบเสนอราคาจะแทนที่การอ้างอิงตัวแปรปิดสำหรับโหนดอ้างอิง ParameterExpression จากนั้นส่งคืนนิพจน์ที่ยกมา […] (น. 63–64)


1
คำตอบที่ยอดเยี่ยมของประเภทการสอนมนุษย์ให้ตกปลา ฉันต้องการเพียงเพื่อเพิ่มว่าเอกสารที่มีการย้ายและตอนนี้สามารถที่docs.microsoft.com/en-us/dotnet/framework/... เอกสารที่ยกมาโดยเฉพาะอยู่บน GitHub: github.com/IronLanguages/dlr/tree/master/Docs
ค่อนข้าง

3

หลังจากนี้คำตอบที่ยอดเยี่ยมจริงๆก็ชัดเจนว่าความหมายคืออะไร ยังไม่ชัดเจนว่าทำไมพวกเขาถึงได้รับการออกแบบเช่นนั้นให้พิจารณา:

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

เมื่อแลมบ์ดานี้ถูกคอมไพล์และเรียกใช้มันจะประเมินนิพจน์ภายในและส่งคืนผลลัพธ์ นิพจน์ภายในนี่คือการเพิ่มดังนั้นps + ptจึงถูกประเมินและผลลัพธ์จะถูกส่งกลับ ตามตรรกะนี้นิพจน์ต่อไปนี้:

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

ควรส่งคืนการอ้างอิงวิธีการคอมไพล์แลมด้าของด้านในเมื่อมีการเรียกแลมด้าภายนอก (เพราะเราบอกว่าแลมบ์ดาคอมไพล์เป็นการอ้างอิงเมธอด) แล้วทำไมเราถึงต้องการใบเสนอราคา! เพื่อแยกความแตกต่างของกรณีเมื่อการอ้างอิงเมธอดถูกส่งกลับเทียบกับผลลัพธ์ของการเรียกใช้การอ้างอิงนั้น

โดยเฉพาะ:

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

เนื่องจากเหตุผลบางประการนักออกแบบ Net จึงเลือกExpression.Quote (f)สำหรับกรณีแรกและfธรรมดาสำหรับกรณีที่สอง ในมุมมองของฉันสิ่งนี้ทำให้เกิดความสับสนอย่างมากเนื่องจากในภาษาโปรแกรมส่วนใหญ่ที่ส่งคืนค่าเป็นแบบตรง (ไม่จำเป็นต้องอ้างใบเสนอราคาหรือการดำเนินการอื่นใด) แต่การเรียกใช้จำเป็นต้องมีการเขียนเพิ่มเติม (วงเล็บ + อาร์กิวเมนต์) ซึ่งแปลเป็นบางประเภทเรียกใช้ที่ระดับ MSIL นักออกแบบ. Net ทำให้มันตรงกันข้ามกับต้นไม้นิพจน์ จะน่าสนใจที่จะทราบเหตุผล


0

ฉันเชื่อว่ามันเป็นเหมือนที่ให้ไว้:

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

ต้นไม้ของคุณเป็นExpression.Lambda(Expression.Lambda)และfแสดงให้เห็นถึงการแสดงออกต้นไม้สำหรับแลมบ์ดาที่ส่งกลับให้ผลตอบแทนFunc<int>2

แต่ถ้าสิ่งที่คุณต้องการคือ lambda ที่ส่งคืนExpression Treeสำหรับ lambda ที่ส่งคืน2คุณต้อง:

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

และตอนนี้ทรีของคุณเป็นExpression.Lambda(Expression.Quote(Expression.Lambda))และfแสดงถึง Expression Tree สำหรับแลมบ์ดาที่ส่งคืนExpression<Func<int>>นั่นคือ Expression Tree สำหรับFunc<int>ผลลัพธ์2นั้น


-2

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


ใช่ไหม? มันเพิ่มการแสดงออกอะไรกันแน่? คุณสามารถ "แสดงออก" อะไรกับ UnaryExpression (ซึ่งเป็นนิพจน์แปลก ๆ ที่จะใช้เช่นกัน) ที่คุณไม่สามารถแสดงออกด้วย ConstantExpression ได้
Timwi
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.