短い答え:
引用演算子は、そのオペランドに閉包意味論を誘発する演算子です。定数は単なる値です。
引用符と定数は意味が異なるため、式ツリーでは表現が異なります。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つのことを意味します。