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


98

참고 : 이전 질문 인 " 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발명 되었습니까?

답변:


189

짧은 답변:

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

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

긴 대답 :

다음을 고려하세요:

(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, 람다를 만들기위한 수십 줄의 리플렉션 코드. 인용 연산자의 목적은 표현식 트리 생성 코드를 명시 적으로 생성하지 않고도 주어진 람다가 함수가 아닌 표현식 트리로 처리되기를 원한다는 것을 표현식 트리 컴파일러에 알리는 것 입니다.

쉬운 방법은 다음과 같습니다.

        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'유형의 변수 's'가 정의되지 않았습니다. "예외가 발생합니다.

(참고 : 방금 인용 된 표현식 트리에서 델리게이트 생성을위한 코드 생성기를 검토했으며, 불행히도 2006 년에 코드에 넣은 주석이 아직 남아 있습니다. 참고로, 호이스트 된 외부 매개 변수는 인용 될 때 상수로 스냅 샷 됩니다. 표현 트리는 런타임 컴파일러에 의해 델리게이트로 수정됩니다.이 시점에서 기억하지 못하는 방식으로 코드를 작성한 이유가 있었지만 외부 매개 변수 에 대해 클로저를 도입하는 불쾌한 부작용이 있습니다. 변수에 대한 종결보다는. 분명히 그 코드를 물려받은 팀은 그 결함을 수정하지 않기로 결정 했으므로 컴파일 된 인용 된 내부 람다에서 관찰되는 닫힌 외부 매개 변수의 변이에 의존한다면 실망하게 될 것입니다. 그러나 (1) 형식 매개 변수를 변경하고 (2) 외부 변수의 변경에 의존하는 것은 매우 나쁜 프로그래밍 관행이므로,이 두 가지 나쁜 프로그래밍 관행을 사용하지 않도록 프로그램을 변경하는 것이 좋습니다. 다가오는 것 같지 않은 수정을 기다리고 있습니다. 오류에 대해 사과드립니다.)

따라서 질문을 반복하려면 :

C # 컴파일러는 중첩 된 람다 식을 Expression.Quote () 대신 Expression.Constant ()와 관련된 식 트리로 컴파일하고 식 트리를 다른 쿼리 언어 (예 : SQL)로 처리하려는 LINQ 쿼리 공급자를 만들 수 있습니다. )는 특수한 Quote 노드 유형이있는 UnaryExpression 대신 Expression 유형이있는 ConstantExpression을 찾을 수 있으며 다른 모든 것은 동일합니다.

당신이 올바른지. 우리는 수단에 의해 "이 값에 고정 된 의미를 유도"는 의미 정보 인코딩 플래그로 일정한 표현 형태를 사용하여 .

"상수"는 "이 상수 값을 사용합니다. , 유형이 표현식 트리 유형 이고 값이 유효한 표현식 트리 인 경우가 아니면 대신 이 상수 값 을 사용합니다. 우리가 지금있을 수있는 외부 람다의 맥락에서 클로저 의미론을 유도하기 위해 주어진 표현 트리의 내부.

그런데 왜 우리는 미친 짓을? 인용 연산자는 엄청나게 복잡한 연산자 이며, 사용 하려는 경우 명시 적 으로 사용해야합니다. 당신은 이미 존재하는 수십 개의 팩토리 메소드와 노드 유형을 추가하지 않는 것에 대해 간결하게하기 위해 상수에 기괴한 코너 케이스를 추가하여 상수가 때때로 논리적으로 상수이고 때로는 다시 작성되도록 제안합니다. 클로저 의미론이있는 람다.

상수가 "이 값 사용"을 의미하지 않는다는 다소 이상한 효과도 있습니다. 위의 세 번째 경우에서 외부 변수에 대한 다시 작성되지 않은 참조가있는 식 트리를 전달하는 대리자로 식 트리를 컴파일하기 를 원하는 기괴한 이유가 있다고 가정 해 보겠습니다 . 왜? 아마도 컴파일러를 테스트 하고 있고 나중에 다른 분석을 수행 할 수 있도록 상수를 전달하기를 원하기 때문일 수 있습니다. 당신의 제안은 그것을 불가능하게 만들 것입니다. 표현식 트리 유형 인 상수는 상관없이 다시 작성됩니다. "상수"는 "이 값 사용"을 의미한다는 합리적인 기대를 가지고 있습니다. "Constant"는 "내 말대로"노드입니다. 상수 프로세서 ' 유형에 따라 말할 수 있습니다.

그리고 당신은 지금 이해의 부담을 가하고 있습니다 것을 물론 노트 (그 정수를 이해하는 것은 평균 한 경우에 "일정"과는 플래그에 따라 "폐쇄 의미 유도"한다는 의미 복잡했다되는 타입 시스템에 따라) 모든를 Microsoft 공급자뿐만 아니라 식 트리의 의미 분석을 수행하는 공급자입니다. 이러한 제 3 자 제공 업체 중 몇 개가 잘못 이해하고 있습니까?

"인용구"는 "안녕 친구, 여기를 봐, 나는 중첩 된 람다 표현식이고 외부 변수에 대해 닫히면 이상한 의미를 가지고 있습니다!"라는 큰 붉은 깃발을 흔들며 흔들고 있습니다. "Constant"는 "나는 가치에 지나지 않습니다. 적합하다고 생각되는대로 저를 사용하십시오." 어떤 것이 복잡하고 위험 할 때 우리는 사용자 가이 값이 특별한 것인지 아닌지를 알아 내기 위해 타입 시스템 을 파헤쳐 서 그 사실을 숨기지 않고 위험 신호를 흔드는 것을 원합니다 .

또한 중복을 피하는 것이 목표라는 생각은 잘못된 것입니다. 물론 불필요하고 혼란스러운 중복성을 피하는 것이 목표이지만 대부분의 중복성은 좋은 것입니다. 중복성은 명확성을 만듭니다. 새로운 팩토리 방법과 노드 종류는 저렴 합니다. 각 작업이 하나의 작업을 깨끗하게 나타내도록 필요한만큼 만들 수 있습니다. 우리는 "이 필드가이 항목으로 설정되지 않는 한 한 가지를 의미합니다.이 경우 다른 것을 의미합니다."와 같은 불쾌한 속임수에 의지 할 필요가 없습니다.


11
클로저 의미론을 생각하지 않았고 중첩 된 람다가 외부 람다에서 매개 변수를 캡처하는 경우를 테스트하지 못했기 때문에 지금 당황합니다. 그렇게했다면 그 차이를 알아 차렸을 것입니다. 귀하의 답변에 다시 한번 감사드립니다.
Timwi

19

이 질문은 이미 훌륭한 답변을 받았습니다. 또한 표현식 트리에 대한 질문에 도움이 될 수있는 리소스를 언급하고 싶습니다.

그곳에 이다 Microsoft의 CodePlex 프로젝트는 동적 언어 런타임. 문서에는 다음과 같은 제목의 문서가 포함됩니다."표현식 트리 v2 사양".NET 4의 LINQ 식 트리에 대한 사양입니다.

업데이트 : CodePlex가 작동하지 않습니다. 사양 (PDF) V2 표현 나무는 GitHub의 이동했습니다 .

예를 들어 다음에 대해 다음과 같이 말합니다 Expression.Quote.

4.4.42 견적

UnaryExpressions에서 Quote를 사용하여 Expression 유형의 "상수"값이있는 표현식을 나타냅니다. Constant 노드와 달리 Quote 노드는 포함 된 ParameterExpression 노드를 특별히 처리합니다. 포함 된 ParameterExpression 노드가 결과 식에서 닫힐 로컬을 선언하면 Quote는 참조 위치에서 ParameterExpression을 대체합니다. Quote 노드가 평가 될 때 런타임에 ParameterExpression 참조 노드에 대한 클로저 변수 참조를 대체 한 다음 인용 된 식을 반환합니다. […] (p. 63–64)


1
Teach-a-man-to-fish 종류의 훌륭한 답변입니다. 문서가 이동되었으며 이제 docs.microsoft.com/en-us/dotnet/framework/… 에서 사용할 수 있음을 추가하고 싶습니다 . 특히 인용 된 문서는 GitHub에 있습니다 : github.com/IronLanguages/dlr/tree/master/Docs
relative_random

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.

그러나 원하는 것이를 반환하는 람다에 대한 식 트리 를 반환하는 람다라면 2다음이 필요합니다.

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

그리고 지금 당신의 나무입니다 Expression.Lambda(Expression.Quote(Expression.Lambda))f반환하는 람다 식 트리를 나타내는 Expression<Func<int>>A에 대한 식 트리입니다 Func<int>그 반환을 2.


-2

여기서 포인트는 나무의 표현력이라고 생각합니다. 대리자를 포함하는 상수 식은 실제로 대리자가되는 개체를 포함합니다. 이것은 단항 및 이진 표현식으로 직접 분해하는 것보다 덜 표현 적입니다.


맞나요? 정확히 어떤 표현력을 더합니까? 이미 ConstantExpression으로 표현할 수 없었던 UnaryExpression (사용하기에는 이상한 종류의 표현)으로 무엇을 "표현"할 수 있습니까?
Timwi
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.