Expression.Quote () làm được gì mà Expression.Constant () chưa thể làm được?


97

Lưu ý: Tôi đã biết câu hỏi trước đó “ Mục đích của phương thức Expression.Quote của LINQ là gì? , Nhưng nếu bạn đọc tiếp bạn sẽ thấy rằng nó không trả lời câu hỏi của tôi.

Tôi hiểu mục đích đã nêu Expression.Quote()là gì. Tuy nhiên, Expression.Constant()có thể được sử dụng cho cùng một mục đích (ngoài tất cả các mục đích Expression.Constant()đã được sử dụng). Vì vậy, tôi không hiểu tại sao lại Expression.Quote()được yêu cầu.

Để chứng minh điều này, tôi đã viết một ví dụ nhanh trong đó người ta thường sử dụng Quote(xem dòng được đánh dấu bằng dấu chấm than), nhưng tôi đã sử dụng Constantthay thế và nó hoạt động tốt như nhau:

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

Đầu ra của expr.ToString()cả hai đều giống nhau (cho dù tôi sử dụng Constanthay Quote).

Với những quan sát trên, có vẻ như điều đó Expression.Quote()là thừa. Trình biên dịch C # có thể đã được thực hiện để biên dịch các biểu thức lambda lồng nhau thành một cây biểu thức liên quan đến Expression.Constant()thay vì Expression.Quote()và bất kỳ nhà cung cấp truy vấn LINQ nào muốn xử lý cây biểu thức thành một số ngôn ngữ truy vấn khác (chẳng hạn như SQL) có thể tìm kiếm ConstantExpressionloại với Expression<TDelegate>thay vì a UnaryExpressionvới Quoteloại nút đặc biệt và mọi thứ khác sẽ giống nhau.

Tôi đang thiếu gì? Tại sao lại được phát minh và loại nút Expression.Quote()đặc biệt ?QuoteUnaryExpression

Câu trả lời:


189

Câu trả lời ngắn:

Nhà điều hành quote là một nhà điều hànhgây ra ngữ nghĩa đóng cửa vào toán hạng của nó . Hằng số chỉ là giá trị.

Dấu ngoặc kép và hằng số có ý nghĩa khác nhau và do đó có các biểu diễn khác nhau trong một cây biểu thức . Có cùng một đại diện cho hai thứ rất khác nhau là cực kỳ khó hiểu và dễ xảy ra lỗi.

Câu trả lời dài:

Hãy xem xét những điều sau:

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

Lambda bên ngoài là một nhà máy cho các bộ cộng được liên kết với tham số của lambda bên ngoài.

Bây giờ, giả sử chúng ta muốn biểu diễn điều này như một cây biểu thức mà sau này sẽ được biên dịch và thực thi. Phần thân của cây biểu hiện phải là gì? Nó phụ thuộc vào việc bạn muốn trạng thái đã biên dịch trả về một đại biểu hay một cây biểu thức.

Hãy bắt đầu bằng cách loại bỏ trường hợp không thú vị. Nếu chúng ta muốn nó trả về một đại biểu thì câu hỏi về việc sử dụng Trích dẫn hay Hằng số là một điểm tranh luận:

        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 có một lambda lồng nhau; trình biên dịch tạo lambda bên trong như một đại biểu cho một hàm được đóng trên trạng thái của hàm được tạo cho lambda bên ngoài. Chúng ta không cần xem xét trường hợp này nữa.

Giả sử chúng ta muốn trạng thái đã biên dịch trả về một cây biểu thức của bên trong. Có hai cách để làm điều đó: cách dễ và cách khó.

Cách khó là nói điều đó thay vì

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

ý chúng tôi thực sự là

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

Và sau đó tạo cây biểu thức cho điều đó , tạo ra mớ hỗn độn này :

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

blah blah blah, hàng chục dòng mã phản chiếu để tạo lambda. Mục đích của toán tử trích dẫn là nói với trình biên dịch cây biểu thức rằng chúng ta muốn lambda đã cho được coi như một cây biểu thức, không phải như một hàm, mà không cần phải tạo mã tạo cây biểu thức một cách rõ ràng .

Cách dễ dàng là:

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

Và thực sự, nếu bạn biên dịch và chạy mã này, bạn sẽ có câu trả lời đúng.

Lưu ý rằng toán tử trích dẫn là toán tử tạo ra ngữ nghĩa đóng trên lambda bên trong sử dụng một biến bên ngoài, một tham số chính thức của lambda bên ngoài.

Câu hỏi là: tại sao không loại bỏ Quote và làm cho điều này làm điều tương tự?

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

Hằng số không tạo ra ngữ nghĩa đóng. Tại sao nên làm thế? Bạn đã nói rằng đây là một hằng số . Nó chỉ là một giá trị. Nó phải hoàn hảo khi được giao cho trình biên dịch; trình biên dịch sẽ có thể chỉ tạo một kết xuất giá trị đó vào ngăn xếp nơi nó cần thiết.

Vì không có bao đóng nào được tạo ra, nếu bạn làm điều này, bạn sẽ nhận được ngoại lệ "biến" thuộc loại 'System.Int32' không được xác định "trên lời gọi.

(Bên cạnh: Tôi vừa xem xét trình tạo mã để tạo đại biểu từ cây biểu thức được trích dẫn và rất tiếc là một nhận xét mà tôi đã đưa vào mã hồi năm 2006 vẫn còn đó. FYI, tham số bên ngoài được lưu trữ được chụp nhanh thành một hằng số khi được trích dẫn Cây biểu thức được trình biên dịch thời gian chạy sửa đổi thành một đại biểu. Có một lý do chính đáng tại sao tôi viết mã theo cách đó mà tôi không nhớ chính xác vào thời điểm này, nhưng nó có tác dụng phụ khó chịu là giới thiệu đóng trên các giá trị của tham số bên ngoài thay vì đóng trên các biến. Rõ ràng nhóm kế thừa mã đó đã quyết định không sửa lỗi đó, vì vậy nếu bạn đang dựa vào sự đột biến của thông số bên ngoài đóng ngoài được quan sát trong lambda nội thất được trích dẫn đã biên dịch, bạn sẽ thất vọng. Tuy nhiên, vì việc (1) biến đổi một tham số chính thức và (2) dựa vào đột biến của một biến bên ngoài là một cách lập trình khá tệ, tôi khuyên bạn nên thay đổi chương trình của mình để không sử dụng hai phương pháp lập trình xấu này, thay vì đang chờ một bản sửa lỗi không có vẻ như sắp xảy ra. Xin lỗi vì lỗi.)

Vì vậy, để lặp lại câu hỏi:

Trình biên dịch C # có thể được thực hiện để biên dịch các biểu thức lambda lồng nhau thành một cây biểu thức liên quan đến Expression.Constant () thay vì Expression.Quote () và bất kỳ trình cung cấp truy vấn LINQ nào muốn xử lý cây biểu thức thành một số ngôn ngữ truy vấn khác (chẳng hạn như SQL ) có thể tìm một ConstantExpression với kiểu Expression thay vì UnaryExpression với kiểu nút Quote đặc biệt và mọi thứ khác sẽ giống nhau.

Bạn nói đúng. Chúng tôi có thể mã hóa thông tin ngữ nghĩa có nghĩa là "tạo ra ngữ nghĩa đóng trên giá trị này" bằng cách sử dụng kiểu của biểu thức hằng làm cờ .

"Hằng số" khi đó sẽ có ý nghĩa "sử dụng giá trị không đổi này, trừ khi kiểu xảy ra là một kiểu cây biểu thức giá trị là một cây biểu thức hợp lệ, trong trường hợp đó, thay vào đó, hãy sử dụng giá trị là cây biểu thức do viết lại bên trong cây biểu thức đã cho để tạo ra ngữ nghĩa đóng trong ngữ cảnh của bất kỳ lambdas bên ngoài nào mà chúng ta có thể đang ở ngay bây giờ.

Nhưng tại sao sẽ chúng tôi làm điều đó điều điên? Toán tử trích dẫn là một toán tử cực kỳ phức tạp và nó nên được sử dụng một cách rõ ràng nếu bạn định sử dụng nó. Bạn đang đề xuất rằng để phân tích cú pháp về việc không thêm một phương thức gốc và kiểu nút bổ sung trong số hàng chục đã có, chúng tôi thêm một trường hợp góc kỳ lạ vào các hằng số, để các hằng số đôi khi là hằng số logic và đôi khi chúng được viết lại lambdas với ngữ nghĩa đóng.

Nó cũng sẽ có một hiệu ứng hơi kỳ lạ rằng hằng số không có nghĩa là "sử dụng giá trị này". Giả sử vì một lý do kỳ lạ nào đó bạn muốn trường hợp thứ ba ở trên biên dịch một cây biểu thức thành một đại biểu đưa ra một cây biểu thức có tham chiếu không được viết lại đến một biến bên ngoài? Tại sao? Có lẽ bởi vì bạn đang thử nghiệm trình biên dịch của mình và muốn chỉ chuyển hằng số qua để bạn có thể thực hiện một số phân tích khác về nó sau này. Đề xuất của bạn sẽ biến điều đó thành không thể; bất kỳ hằng số nào xảy ra thuộc loại cây biểu thức sẽ được viết lại bất kể. Người ta có một kỳ vọng hợp lý rằng "không đổi" có nghĩa là "sử dụng giá trị này". "Constant" là một nút "làm những gì tôi nói". Bộ xử lý không đổi ' để nói dựa trên loại.

Và tất nhiên hãy lưu ý rằng bây giờ bạn đang đặt gánh nặng hiểu biết (nghĩa là, hiểu rằng hằng số có ngữ nghĩa phức tạp có nghĩa là "hằng số" trong một trường hợp và "tạo ra ngữ nghĩa đóng" dựa trên một cờ nằm trong kiểu hệ thống ) trên mỗi trình cung cấp thực hiện phân tích ngữ nghĩa của cây biểu thức, không chỉ dựa trên các nhà cung cấp của Microsoft. Có bao nhiêu trong số các nhà cung cấp bên thứ ba đó sẽ làm sai?

"Trích dẫn" đang vẫy một lá cờ đỏ lớn nói rằng "này anh bạn, nhìn qua đây, tôi là một biểu thức lambda lồng nhau và tôi có ngữ nghĩa kỳ quặc nếu tôi đóng trên một biến bên ngoài!" trong khi "Constant" nói "Tôi không hơn gì một giá trị; hãy sử dụng tôi khi bạn thấy phù hợp." Khi một thứ gì đó phức tạp và nguy hiểm, chúng tôi muốn làm cho nó nổi lên những lá cờ đỏ, chứ không phải che giấu sự thật đó bằng cách bắt người dùng tìm hiểu hệ thống loại để tìm xem giá trị này có phải là giá trị đặc biệt hay không.

Hơn nữa, ý kiến ​​cho rằng tránh dư thừa thậm chí là một mục tiêu là không chính xác. Chắc chắn, tránh dư thừa không cần thiết, khó hiểu là một mục tiêu, nhưng hầu hết dư thừa là một điều tốt; sự dư thừa tạo ra sự rõ ràng. Các phương pháp nhà máy mới và các loại nút rẻ . Chúng tôi có thể tạo ra bao nhiêu tùy thích để mỗi cái đại diện cho một hoạt động rõ ràng. Chúng ta không cần phải dùng đến những thủ thuật khó chịu như "điều này có nghĩa là một thứ trừ khi trường này được đặt thành thứ này, trong trường hợp đó nó có nghĩa là một thứ khác."


11
Bây giờ tôi lúng túng vì tôi không nghĩ đến ngữ nghĩa đóng và không kiểm tra được trường hợp lambda lồng nhau nắm bắt một tham số từ lambda bên ngoài. Nếu tôi đã làm điều đó, tôi sẽ nhận thấy sự khác biệt. Rất cám ơn một lần nữa cho câu trả lời của bạn.
Timwi 21/09/10

19

Câu hỏi này đã nhận được một câu trả lời xuất sắc. Ngoài ra, tôi muốn chỉ đến một tài nguyên có thể hữu ích với các câu hỏi về cây biểu thức:

Đó là một dự án CodePlex của Microsoft được gọi là Thời gian chạy ngôn ngữ động. Tài liệu của nó bao gồm tài liệu có tiêu đề,"Cây biểu hiện v2 Spec", chính xác là: Đặc tả cho cây biểu thức LINQ trong .NET 4.

Cập nhật: CodePlex không còn tồn tại. Thông số kỹ thuật của Cây biểu hiện v2 (PDF) đã được chuyển sang GitHub .

Ví dụ, nó nói như sau về Expression.Quote:

4.4.42 Trích dẫn

Sử dụng Quote trong UnaryExpressions để biểu diễn một biểu thức có giá trị "không đổi" của loại Biểu thức. Không giống như nút Constant, nút Quote đặc biệt xử lý các nút ParameterExpression có chứa. Nếu một nút ParameterExpression được chứa khai báo một cục bộ sẽ được đóng lại trong biểu thức kết quả, thì Trích dẫn sẽ thay thế ParameterExpression trong các vị trí tham chiếu của nó. Tại thời điểm chạy khi nút Quote được đánh giá, nó sẽ thay thế các tham chiếu biến đóng cho các nút tham chiếu ParameterExpression, rồi trả về biểu thức được trích dẫn. […] (Trang 63–64)


1
Câu trả lời tuyệt vời của loại dạy-một-người-với-cá. Tôi chỉ muốn nói thêm rằng tài liệu đã được chuyển đi và hiện có tại docs.microsoft.com/en-us/dotnet/framework/… . Cụ thể, tài liệu được trích dẫn có trên GitHub: github.com/IronLanguages/dlr/tree/master/Docs
tương

3

Sau câu trả lời thực sự xuất sắc này, rõ ràng là ngữ nghĩa là gì. Không rõ tại sao chúng được thiết kế theo cách đó, hãy xem xét:

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

Khi lambda này được biên dịch và gọi, nó sẽ đánh giá biểu thức bên trong và trả về kết quả. Biểu thức bên trong ở đây là một phép cộng, vì vậy ps + pt được đánh giá và kết quả được trả về. Theo logic này, biểu thức sau:

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

nên trả về tham chiếu phương thức đã biên dịch lambda của bên trong khi lambda bên ngoài được gọi (vì chúng ta nói rằng lambda biên dịch thành tham chiếu phương thức). Vậy tại sao chúng ta cần một Trích dẫn ?! Để phân biệt trường hợp khi tham chiếu phương thức được trả về so với kết quả của lệnh gọi tham chiếu đó.

Đặc biệt:

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

Do một số lý do mà các nhà thiết kế .Net đã chọn Expression.Quote (f) cho trường hợp đầu tiên và f trơn cho trường hợp thứ hai. Để quan điểm của tôi điều này gây ra rất nhiều rắc rối, vì trong hầu hết các ngôn ngữ lập trình trả lại một giá trị trực tiếp (không cần Quote hoặc bất kỳ hoạt động khác), nhưng gọi không yêu cầu thêm bằng văn bản (ngoặc + đối số), mà dịch để một số loại gọi ở cấp MSIL. Các nhà thiết kế .Net đã làm ngược lại với các cây biểu thức. Sẽ rất thú vị khi biết lý do.


-2

Tôi nghĩ điểm mấu chốt ở đây là tính biểu cảm của cây. Biểu thức hằng có chứa đại biểu thực sự chỉ chứa một đối tượng xảy ra là một đại biểu. Điều này ít biểu đạt hơn là phân tích trực tiếp thành một biểu thức một ngôi và nhị phân.


Là nó? Chính xác thì nó thêm vào biểu cảm nào? Bạn có thể “diễn đạt” điều gì với UnaryExpression đó (cũng là một loại biểu thức kỳ lạ để sử dụng) mà bạn chưa thể diễn đạt với ConstantExpression?
Timwi
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.