Programing

Expression.Quote ()는 Expression.Constant ()가 할 수없는 일을 무엇입니까?

crosscheck 2020. 9. 1. 07:03
반응형

Expression.Quote ()는 Expression.Constant ()가 할 수없는 일을 무엇입니까?


참고 : 이전 질문 인 " LINQ의 Expression.Quote 메서드의 목적은 무엇입니까? "를 알고 있습니다 . ,하지만 읽으면 내 질문에 답이 없다는 것을 알게 될 것입니다.

의 명시된 목적이 무엇인지 이해합니다 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()다른 쿼리 언어 (예 : SQL)로 처리하려는 모든 LINQ 쿼리 공급자는 대신 ConstantExpressionwith 형식을 찾을 수 있습니다. 특별와 노드 유형 및 다른 모든 같은 것이다.Expression<TDelegate>UnaryExpressionQuote

내가 무엇을 놓치고 있습니까? Expression.Quote()그리고 특별한 Quote노드 유형이 UnaryExpression발명 되었습니까?


짧은 답변:

따옴표 연산자는 피연산자에 클로저 의미유도 하는 연산자 입니다 . 상수는 값일뿐입니다.

따옴표와 상수는 의미 가 다르 므로 식 트리에서 다른 표현을 갖습니다 . 매우 다른 두 가지에 대해 동일한 표현을 갖는 것은 매우 혼란스럽고 버그가 발생하기 쉽습니다.

긴 대답 :

다음을 고려하세요:

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

외부 람다는 외부 람다의 매개 변수에 바인딩 된 가산기의 팩토리입니다.

이제 이것을 나중에 컴파일하고 실행할 표현식 트리로 표현하고 싶다고 가정합니다. 표현 트리의 본문은 무엇이어야합니까? 컴파일 된 상태에서 대리자 또는 식 트리를 반환할지 여부에 따라 다릅니다.

흥미롭지 않은 사건을 기각하는 것으로 시작합시다. 델리게이트를 반환하려면 Quote 또는 Constant를 사용할지 여부에 대한 질문은 논점입니다.

        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, dozens of lines of reflection code to make the lambda. The purpose of the quote operator is to tell the expression tree compiler that we want the given lambda to be treated as an expression tree, not as a function, without having to explicitly generate the expression tree generation code.

The easy way is:

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

And indeed, if you compile and run this code you get the right answer.

Notice that the quote operator is the operator which induces closure semantics on the interior lambda which uses an outer variable, a formal parameter of the outer lambda.

The question is: why not eliminate Quote and make this do the same thing?

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

The constant does not induce closure semantics. Why should it? You said that this was a constant. It's just a value. It should be perfect as handed to the compiler; the compiler should be able to just generate a dump of that value to the stack where it is needed.

Since there is no closure induced, if you do this you'll get a "variable 's' of type 'System.Int32' is not defined" exception on the invocation.

(Aside: I've just reviewed the code generator for delegate creation from quoted expression trees, and unfortunately a comment that I put into the code back in 2006 is still there. FYI, the hoisted outer parameter is snapshotted into a constant when the quoted expression tree is reified as a delegate by the runtime compiler. There was a good reason why I wrote the code that way which I do not recall at this exact moment, but it does have the nasty side effect of introducing closure over values of outer parameters rather than closure over variables. Apparently the team which inherited that code decided to not fix that flaw, so if you are relying upon mutation of a closed-over outer parameter being observed in a compiled quoted interior lambda, you're going to be disappointed. However, since it is a pretty bad programming practice to both (1) mutate a formal parameter and (2) rely upon mutation of an outer variable, I would recommend that you change your program to not use these two bad programming practices, rather than waiting for a fix which does not appear to be forthcoming. Apologies for the error.)

So, to repeat the question:

The C# compiler could have been made to compile nested lambda expressions into an expression tree involving Expression.Constant() instead of Expression.Quote(), and any LINQ query provider that wants to process expression trees into some other query language (such as SQL) could look out for a ConstantExpression with type Expression instead of a UnaryExpression with the special Quote node type, and everything else would be the same.

You are correct. We could encode semantic information that means "induce closure semantics on this value" by using the type of the constant expression as a flag.

"Constant" would then have the meaning "use this constant value, unless the type happens to be an expression tree type and the value is a valid expression tree, in which case, instead use the value that is the expression tree resulting from rewriting the interior of the given expression tree to induce closure semantics in the context of any outer lambdas that we might be in right now.

But why would we do that crazy thing? The quote operator is an insanely complicated operator, and it should be used explicitly if you're going to use it. You're suggesting that in order to be parsimonious about not adding one extra factory method and node type amongst the several dozen already there, that we add a bizarre corner case to constants, so that constants are sometimes logically constants, and sometimes they are rewritten lambdas with closure semantics.

It would also have the somewhat odd effect that constant doesn't mean "use this value". Suppose for some bizarre reason you wanted the third case above to compile an expression tree into a delegate that hands out an expression tree that has a not-rewritten reference to an outer variable? Why? Perhaps because you are testing your compiler and want to just pass the constant on through so that you can perform some other analysis on it later. Your proposal would make that impossible; any constant that happens to be of expression tree type would be rewritten regardless. One has a reasonable expectation that "constant" means "use this value". "Constant" is a "do what I say" node. The constant processor's job is not to guess at what you meant to say based on the type.

And note of course that you are now putting the burden of understanding (that is, understanding that constant has complicated semantics that mean "constant" in one case and "induce closure semantics" based on a flag that is in the type system) upon every provider that does semantic analysis of an expression tree, not just upon Microsoft providers. How many of those third-party providers would get it wrong?

"Quote" is waving a big red flag that says "hey buddy, look over here, I'm a nested lambda expression and I have wacky semantics if I'm closed over an outer variable!" whereas "Constant" is saying "I'm nothing more than a value; use me as you see fit." When something is complicated and dangerous we want to be making it wave red flags, not hiding that fact by making the user dig through the type system in order to find out whether this value is a special one or not.

Furthermore, the idea that avoiding redundancy is even a goal is incorrect. Sure, avoiding unnecessary, confusing redundancy is a goal, but most redundancy is a good thing; redundancy creates clarity. New factory methods and node kinds are cheap. We can make as many as we need so that each one represents one operation cleanly. We have no need to resort to nasty tricks like "this means one thing unless this field is set to this thing, in which case it means something else."


This question has already received an excellent answer. I'd additionally like to point to a resource that can prove helpful with questions about expression trees:

There is a CodePlex project by Microsoft called Dynamic Language Runtime. Its documentation includes the document titled, "Expression Trees v2 Spec", which is exactly that: The specification for LINQ expression trees in .NET 4.

For example, it says the following about Expression.Quote:

4.4.42 Quote

Use Quote in UnaryExpressions to represents an expression that has a "constant" value of type Expression. Unlike a Constant node, the Quote node specially handles contained ParameterExpression nodes. If a contained ParameterExpression node declares a local that would be closed over in the resulting expression, then Quote replaces the ParameterExpression in its reference locations. At run time when the Quote node is evaluated, it substitutes the closure variable references for the ParameterExpression reference nodes, and then returns the quoted expression. […] (p. 63–64)


After this a really excellent answer, it's clear what are the semantics. It's not so clear why they are designed that way, consider:

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

When this lambda is compiled and invoked it evaluates the inner expression and returns the result. Inner expression here is an addition, so the ps+pt is evaluated and the result is returned. Following this logic, the following expression:

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

should return an inner's lambda compiled method reference when the outer lambda is invoked (because we say that lambda compiles to a method reference). So why do we need a Quote?! To differentiate the case when the method reference is returned vs. the result of that reference invocation.

Specifically:

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

Due to some reason .Net designers chose Expression.Quote(f) for the first case and plain f for the second. To my view this causes a great deal of confusion, since in most programming languages returning a value is direct (no need for Quote or any other operation), but invocation does require extra writing (parentheses + arguments), which translates to some kind of invoke at MSIL level. .Net designers made it the opposite for the expression trees. Would be interesting to know the reason.


I think the point here is the expressiveness of the tree. The constant expression containing the delegate is really just containing an object that happens to be a delegate. This is less expressive than a direct breakdown to a unary and binary expression.

참고URL : https://stackoverflow.com/questions/3716492/what-does-expression-quote-do-that-expression-constant-can-t-already-do

반응형