注意:我知道之前的問題“LINQ的Expression.Quote方法的目的是什麼? â€,但如果你讀,你會看到它doesn’噸回答我的問題。
我理解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#編譯器將嵌套的lambda表達式編譯成一個表達式樹,該表達式涉及Expression.Constant()
而不是Expression.Quote()
,以及任何想要將表達式樹處理成其他查詢語言的LINQ查詢提供程序(例如SQL) )可以查找類型為Expression<TDelegate>
的ConstantExpression
,而不是具有特殊Quote
節點類型的UnaryExpression
,其他所有內容都是相同的。
我錯過了什麼?為什麼發明了Expression.Quote()
和UnaryExpression
的特殊Quote
節點類型?
quote運算符是一個操作符 ,它在其操作數上引入閉包語義 。常量只是值。
引號和常量具有不同的含義 ,因此在表達式樹中具有不同的表示 。對兩個非常不同的事物具有相同的表示是非常混亂和容易出錯的。
考慮以下:
(int s)=>(int t)=>s+t
外部lambda是綁定到外部lambda參數的加法器的工廠。
現在,假設我們希望將其表示為稍後將被編譯和執行的表達式樹。表達樹的主體應該是什麼? 這取決於您是否希望編譯狀態返回委託或表達式樹。
讓我們首先解僱這個無趣的案例。如果我們希望它返回一個委託,那麼是否使用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));
lambda有一個嵌套的lambda;編譯器生成內部lambda作為一個函數的委託,該函數關閉為外部lambda生成的函數的狀態。我們不再需要考慮這個案例了。
假設我們希望編譯狀態返回內部的表達式樹 。有兩種方法可以做到:簡單方法和艱難方式。
困難的方式是說而不是
(int s)=>(int t)=>s+t
我們真正的意思是
(int s)=>Expression.Lambda(Expression.Add(...
然後生成的表達式樹,產生這個爛攤子 :
Expression.Lambda(
Expression.Call(typeof(Expression).GetMethod("Lambda", ...
等等,等等幾十行反射代碼來製作lambda。 quote運算符的目的是告訴表達式樹編譯器我們希望將給定的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));
事實上,如果您編譯並運行此代碼,您將得到正確的答案。
請注意,引號運算符是在內部lambda上引入閉包語義的運算符,它使用外部變量,外部lambda的形式參數。
問題是:為什麼不消除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'沒有定義“調用異常。
(旁白:我剛剛從引用的表達式樹中檢查了代碼創建的代碼生成器,不幸的是,我在2006年放入代碼的註釋仍然存在。僅供參考,提升的外部參數在引用時被快照成常量表達式樹由運行時編譯器作為委託來實現。有一個很好的理由我為什麼編寫代碼的方式我在這個時刻不記得,但它確實有引入閉包外部參數值的令人討厭的副作用而不是關閉變量 。顯然,繼承該代碼的團隊決定不修復該缺陷,所以如果你依賴於在編譯的引用內部lambda中觀察到的封閉外部參數的變異,你會感到失望但是,由於(1)改變形式參數和(2)依賴外部變量的變異是一個非常糟糕的編程習慣,我建議你改變你的程序不要使用這兩個壞編程實踐,而不是等待似乎沒有即將到來的修復。為錯誤道歉。)
所以,重複一下這個問題:
可以使用C#編譯器將嵌套的lambda表達式編譯成一個表達式樹,該表達式涉及Expression.Constant()而不是Expression.Quote(),以及任何想要將表達式樹處理成其他查詢語言的LINQ查詢提供程序(例如SQL) )可以查找具有Expression類型的ConstantExpression而不是具有特殊Quote節點類型的UnaryExpression,其他所有內容都是相同的。
你是對的。我們可以 使用常量表達式的類型作為標誌 來編碼語義信息,這意味著“在此值上引發閉包語義”。
然後,“常量”將具有“使用此常量值”的含義, 除非該類型恰好是表達式樹類型且值是有效的表達式樹,在這種情況下,請使用由重寫表達式而得到的表達式樹的值給定表達式樹的內部,以便在我們現在可能處於的任何外部lambda的上下文中引發閉包語義。
但是,我們為什麼要那麼做瘋狂的事? 引號運算符是一個非常複雜的運算符 ,如果您要使用它,應該明確使用它。你建議為了簡單地在幾十個已經存在的情況下不添加一個額外的工廠方法和節點類型,我們在常量中添加了一個奇怪的角點情況,因此常量有時是邏輯常量,有時它們會被重寫具有閉包語義的lambdas。
它也有一些奇怪的效果,常數並不意味著“使用這個值”。假設出於某些奇怪的原因,您希望上面的第三種情況將表達式樹編譯成一個委託,該委託發出一個表達式樹,該表達式對外部變量有一個未重寫的引用?為什麼?也許是因為您正在測試您的編譯器並希望只是傳遞常量,以便您以後可以對其執行一些其他分析。你的建議會讓那不可能;任何恰好是表達式樹類型的常量都將被重寫。人們有一個合理的期望,“常數”意味著“使用這個價值”。 “常數”是“做我說的”節點。恆定處理器的工作不是根據類型猜測你的意思 。
並注意當然,你現在把認識的負擔(即理解是不斷有複雜的,在一個案例中的意思是“不變”和“誘導封閉語義”的基礎上的標誌是在類型系統語義)後, 每對錶達式樹進行語義分析的提供程序,而不僅僅是Microsoft提供程序。 有多少第三方提供商會弄錯?
“引用”正在揮動一個大紅旗,上面寫著“嘿伙計,看看這裡,我是一個嵌套的lambda表達式,如果我關閉外部變量,我就會有古怪的語義!”而“常數”則說“我只不過是一種價值;在你認為合適時使用我。”當某些東西變得複雜和危險時,我們想讓它揮動紅旗,而不是通過讓用戶挖掘類型系統來隱藏這個事實,以便找出這個值是否是特殊值。
此外,避免冗餘甚至是目標的想法是不正確的。當然,避免不必要的,混亂的冗餘是一個目標,但大多數冗餘是一件好事;冗餘創造了清晰度新的工廠方法和節點種類很便宜 。我們可以根據需要製作多個,以便每個人乾淨地代表一個操作。我們沒有必要採取令人討厭的技巧,“這意味著一件事,除非這個領域設置為這個東西,在這種情況下,它意味著其他東西。”
這個問題已經收到了很好的答案。我還想指出一個資源,它可以對錶達式樹的問題有所幫助:
那裡是是微軟的CodePlex項目,名為 動態語言運行時 。其文檔包括標題為“ “表達樹v2規範” ,即:.NET 4中LINQ表達式樹的規範。
更新: CodePlex已失效。 Expression Trees v2規範(PDF)已移至GitHub 。
例如,它對Expression.Quote
表示以下內容:
4.4.42報價
在UnaryExpressions中使用Quote表示具有類型Expression的“常量”值的表達式。與Constant節點不同,Quote節點專門處理包含的ParameterExpression節點。如果包含的ParameterExpression節點聲明了將在結果表達式中關閉的局部,則Quote會在其引用位置替換ParameterExpression。在運行時評估Quote節點時,它將閉包變量引用替換為ParameterExpression引用節點,然後返回帶引號的表達式。 […] (第63–64頁)