web-dev-qa-db-ja.com

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()を含む式ツリーにコンパイルし、式ツリーを他のクエリ言語に処理したいLINQクエリプロバイダーにコンパイルするように作成されている可能性があります。 (SQLなど)は、特殊なConstantExpressionノードタイプのUnaryExpressionの代わりに、タイプ_Expression<TDelegate>_のQuoteを探すことができ、それ以外はすべて同じ。

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

93
Timwi

短い答え:

引用演算子はoperatorで、これはそのオペランドにクロージャセマンティクスを導入するです。定数は単なる値です。

引用符と定数には異なる意味があり、したがって式ツリーでは異なる表現になります。 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(...

次にthatの式ツリーを生成し、-this messを生成します。

        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年にコードに追加したコメントがまだ残っています。参考までに、ホイストされた外部パラメーターはsnapshottedです。 =引用符で囲まれた式ツリーがランタイムコンパイラによってデリゲートとして具体化されるとき、定数に変換します。この時点では思い出せないコードをそのように記述した正当な理由がありましたが、これには次のような厄介な副作用がありますvaluesではなく外部パラメーターのクロージャーを導入variablesではなく、外部パラメーターのクロージャーを導入しているようです。そのコードを継承したチームは、その欠陥を修正しないことにしました。コンパイルされた引用符付きの内部ラムダで閉じられた外側のパラメーターが観察されると、がっかりすることになります。ただし、(1)仮パラメーターを変更することと、(2)の変更に依存することは、かなり悪いプログラミング手法であるためです。外部変数。プログラムをnoに変更することをお勧めします今後予定されていないように思われる修正を待つのではなく、これら2つの悪いプログラミング手法を使用してください。エラーについてお詫びします。)

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

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

あなたは正しいです。 could「この値にクロージャセマンティクスを誘導する」という意味の情報を定数式のタイプをフラグとして使用するでエンコードします。

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

しかし、なぜwould私たちはそのクレイジーなことをするのでしょうか? 引用演算子はめちゃくちゃ複雑な演算子であり、使用する場合はexplicitlyを使用する必要があります。すでにそこに存在する数十の中で1つの余分なファクトリメソッドとノードタイプを追加しないことを節約するために、定数に奇妙なコーナーケースを追加して、定数が論理的に定数になることもあれば、書き直されることもあると提案しています。閉鎖セマンティクスを持つラムダ。

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

そしてもちろん、あなたは今理解の負担をかけていることに注意してください(つまり、定数はある場合には「定数」を意味する複雑なセマンティクスを持ち、system)on every Microsoftプロバイダーだけでなく、式ツリーのセマンティック分析を行うプロバイダー。 それらのサードパーティプロバイダーの何人がそれを誤解するでしょうか?

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

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

185
Eric Lippert

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

そこ です マイクロソフトによるCodePlexプロジェクトでした 動的言語ランタイム。そのドキュメントには、タイトルの付いたドキュメントが含まれています。 "Expression Trees v2 Spec"、それはまさにそれです:.NET 4のLINQ式ツリーの仕様。

更新:CodePlexは機能していません。 Expression Trees v2 Spec(PDF)はGitHub に移動しました。

たとえば、Expression.Quote

4.4.42引用

UnaryExpressionsで引用を使用して、式の「定数」値を持つ式を表します。 Constantノードとは異なり、Quoteノードは、含まれているParameterExpressionノードを特別に処理します。含まれているParameterExpressionノードが、結果の式で閉じられるローカルを宣言する場合、Quoteは参照場所のParameterExpressionを置き換えます。実行時にQuoteノードが評価されると、ParameterExpression参照ノードをクロージャー変数参照に置き換え、引用符で囲まれた式を返します。 […](p。63–64)

これが本当に素晴らしい答えになった後、セマンティクスが何であるかは明らかです。 なぜそれらがそのように設計されているのかはそれほど明確ではありません、考慮してください:

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を選択しました。私の見解では、ほとんどのプログラミング言語では値を返すのは直接的なため(引用符やその他の操作は不要)、これは大きな混乱を引き起こしますが、呼び出しには追加の書き込み(括弧+引数)が必要です、これは、MSILレベルで何らかのinvokeに変換されます。 .Netデザイナーは、これを式ツリーの反対にしています。その理由を知ることは興味深いでしょう。

2