Expression.Quote()は、Expression.Constant()がまだ実行できないことを何を行うのですか?


97

注:以前の質問「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()は冗長であるように見えます。ネストされたラムダ式をのExpression.Constant()代わりに式ツリーにコンパイルするようにC#コンパイラーを作成できます。式ツリーをExpression.Quote()他のクエリ言語(SQLなど)に処理したいLINQクエリプロバイダーは、代わりにConstantExpressionwith型を探します。Expression<TDelegate>a UnaryExpressionは特別なQuoteノードタイプで、それ以外はすべて同じです。

何が欠けていますか?なぜExpression.Quote()、そして発明された特別なQuoteノードタイプなのUnaryExpressionですか?

回答:


189

短い答え:

引用演算子は、そのオペランドに閉包意味論誘発する演算子です。定数は単なる値です。

引用符と定数は意味が異なるため、式ツリーでは表現異なります。2つの非常に異なるものに同じ表現を使用すると、非常に混乱し、バグが発生しやすくなります。

長い答え:

以下を検討してください。

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

ラムダにはネストされたラムダがあります。コンパイラーは、外側のラムダ用に生成された関数の状態に対して閉じられた関数へのデリゲートとして、内側のラムダを生成します。このケースをこれ以上考慮する必要はありません。

コンパイルされた状態が内部の式ツリーを返すようにしたいとします。それには、簡単な方法と難しい方法の2つの方法があります。

難しい方法は、代わりに

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

私たちが本当に意味することは

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

そしてのための式ツリー生成することを生産、この混乱を

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

何とか何とか何とか、ラムダを作成するための何十行ものリフレクションコード。 引用演算子の目的は、式ツリーの生成コードを明示的に生成せずに、特定のラムダを関数ではなく式ツリーとして処理することを式ツリーコンパイラーに指示することです

簡単な方法は次のとおりです。

        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)外部変数の変更に依存することはどちらもかなり悪いプログラミング手法であるため、プログラムを変更して、これら2つの悪いプログラミング手法を使用しないようにすることをお勧めします。来ていないように見える修正を待っています。エラーについてお詫びします。)

だから、質問を繰り返すには:

C#コンパイラは、ネストされたラムダ式を、Expression.Quote()の代わりにExpression.Constant()を含む式ツリーにコンパイルし、他のクエリ言語(SQLなど)に式ツリーを処理したいLINQクエリプロバイダーにコンパイルするように作成されている可能性があります。 )は、特別なQuoteノードタイプのUnaryExpressionの代わりに、タイプがExpressionのConstantExpressionを探すことができ、それ以外はすべて同じです。

あなたは正しいです。私たちは、可能性の手段により「この値に閉鎖セマンティクスを誘導する」という意味情報エンコードフラグとして定数式のタイプを使用します

「定数」は、「タイプがたまたま式ツリータイプであり、値が有効な式ツリーである場合を除き、この定数値を使用する」という意味になります。その場合、代わりに、与えられた式ツリーの内部で、現在存在している可能性のあるすべての外部ラムダのコンテキストでクロージャセマンティクスを誘導します。

しかし、なぜだろう、私たちはその狂気のことを行いますか?クォート演算子はめちゃくちゃ複雑な演算子であり、使用する場合は明示的に使用する必要があります。すでにそこに存在する数十の中で1つの追加のファクトリメソッドとノードタイプを追加しないことを節約するために、定数に奇妙なコーナーケースを追加して、定数が論理的に定数になることもあれば、書き換えられることもあると示唆しています。閉鎖セマンティクスを持つラムダ。

また、定数が「この値を使用する」ことを意味しないという、やや奇妙な効果もあります。なんらかの奇妙な理由で、上の3番目のケースで、式ツリーを、外部変数への書き換えられていない参照を持つ式ツリーを渡すデリゲートにコンパイルしたいとしますか?どうして?おそらく、コンパイラーテストしていて、後で定数を他の分析を実行できるように定数をそのまま渡したいと思うからです。あなたの提案はそれを不可能にします。たまたま式ツリー型の定数は、関係なく書き換えられます。「一定」は「この値を使用する」ことを意味すると合理的に期待しています。「定数」は「私が言うことを行う」ノードです。一定のプロセッサ」 タイプに基づいて言うと。

そして、あなたは今(、である定数が1の場合の平均「定数」という意味を複雑となるフラグに基づいて「閉鎖セマンティクスを誘導」していることを理解理解の負担をかけていることはもちろん、ノート型システムで時)のすべて Microsoftプロバイダーだけでなく、式ツリーのセマンティック分析を行うプロバイダー。それらのサードパーティプロバイダーのどれがそれを誤解するでしょうか?

「Quote」は、「バディ、こっちを見て、ネストされたラムダ式で、外部変数で閉じていると奇妙なセマンティクスを持っている!」という大きな赤い旗を振っています。一方、「定数」は「私は単なる価値であり、あなたが適切だと思うように私を使ってください」と言っています。何かが複雑で危険なときは、この値が特殊な値であるかどうかを調べるためにユーザーに型システムを掘り下げさせてその事実を隠さないようにしたいのです。

さらに、冗長性を回避することが目標でさえあるという考えは正しくありません。確かに、不必要で混乱を招く冗長性を回避することが目標ですが、ほとんどの冗長性は良いことです。冗長性は明快さを作成します。新しいファクトリメソッドとノードの種類は安価です。それぞれが1つの操作をきれいに表すように、必要な数だけ作成できます。「このフィールドがこのものに設定されていない限り、これは1つのことを意味します。


11
クロージャーのセマンティクスについて考えず、ネストされたラムダが外側のラムダからパラメーターをキャプチャするケースをテストできなかったので、今は恥ずかしいです。私がそれをしていたら、私は違いに気づいたでしょう。回答ありがとうございます。
Timwi

19

この質問はすでに優れた回答を得ています。さらに、式ツリーに関する質問に役立つことが証明できるリソースを指摘したいと思います。

そこ です マイクロソフトによるCodePlexプロジェクトでした 動的言語ランタイム。そのドキュメントには、タイトルの付いたドキュメントが含まれています。「Expression Trees 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
魚を教える人の種類の優れた答え。ドキュメントを移動し、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)を選択し、2番目のケースにプレーンfを選択しました。私の見解では、ほとんどのプログラミング言語で値を返すのは直接(Quoteやその他の操作は必要ありません)なので、これは大きな混乱を引き起こしますが、呼び出しには追加の書き込み(括弧+引数)が必要です。MSILレベルで呼び出します。.Netデザイナーは、これを式ツリーの反対にしています。その理由を知ることは興味深いでしょう。


-2

ここでのポイントは木の表現力だと思います。デリゲートを含む定数式は、実際にはデリゲートであるオブジェクトを含むだけです。これは、単項および二項式に直接分解するよりも表現力が劣ります。


それは...ですか?正確にはどのような表現力が追加されますか?既にConstantExpressionで表現できなかったそのUnaryExpression(これも使用する奇妙な種類の表現)で「表現」できるものは何ですか?
ティムウィ2010
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.