如何對 Expression 進行計算?

想解決一個EF6中 Expression 合併的問題,從 stackoverflow 搜到可用的結果 Combining two expressions (Expression&&>)

源碼如此:

public static Expression&&> AndAlso&(
this Expression&&> expr1,
Expression&&> expr2)
{
var parameter = Expression.Parameter(typeof (T));

var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter);
var left = leftVisitor.Visit(expr1.Body);

var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter);
var right = rightVisitor.Visit(expr2.Body);

return Expression.Lambda&&>(
Expression.AndAlso(left, right), parameter);
}

private class ReplaceExpressionVisitor : ExpressionVisitor
{
private readonly Expression _oldValue;
private readonly Expression _newValue;

public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)
{
_oldValue = oldValue;
_newValue = newValue;
}

public override Expression Visit(Expression node)
{
// 這裡意味著什麼?
if (node == _oldValue)
return _newValue;
return base.Visit(node);
}
}

其中,當Body == Parameters[0] 時,使用新的ParameterExpression 。

我有三個問題:

1、兩者相等意味著什麼?為什麼這樣處理?

2、如果我改寫成OrElse,這裡是不是有影響?

3、如果想深入學習該方面的知識,有沒有推薦的書籍?


@Ivony 大大的回答正解了。

俺就來直接回答一下題主的那幾個問題:

其中,當Body == Parameters[0] 時,使用新的ParameterExpression 。

我有三個問題:

1、兩者相等意味著什麼?為什麼這樣處理?

並不是Body == Parameters[0],而是Expression&.Body所指向的expression tree中任何節點與這個指定的Parameters[0]相同時,在visitor返回的新expression tree中將這個節點替換為新的ParameterExpression。

這個操作的意思是什麼 @Ivony 大大的回答已經說得很清楚了。

expr1與expr2的類型都是Expression&&>,換句話說這個lambda的函數類型是接受一個類型為T的參數、返回值類型為bool的形式。所以在它們裡面肯定只會有1個ParameterExpression,也就是由它們的Parameters[0]所引用的那個。

要提取出expr1與expr2的函數體,將它們組裝出一個新的lambda,就必須要把它們的參數替換為新lambda的參數,否則無法建立起新lambda的參數與expr1/expr2已有的函數體之間的聯繫。

其實要組裝expr1與expr2,除了提取出它們的函數體然後直接拼裝之外,還可以選擇直接調用它們然後組合結果,例如說:

using System;
using System.Linq.Expressions;

static class Program {
static Expression&&> AndAlso&(
this Expression&&> expr1,
Expression&&> expr2) {
var param = Expression.Parameter(typeof(T));
var invoke1 = Expression.Invoke(expr1, param);
var invoke2 = Expression.Invoke(expr2, param);
var combine = Expression.AndAlso(invoke1, invoke2);
return Expression.Lambda&&>(combine, param);
}

static void Main(string[] args) {
Expression&&> expr1 = a =&> a &>= 0;
Expression&&> expr2 = b =&> b &< 10; var combined = expr1.AndAlso(expr2); var func = combined.Compile(); Console.WriteLine("{0}, {1}", func(2), func(42)); // prints: // True, False } }

這個例子中,

expr1是

a =&> a &>= 0

expr2 是

b =&> b &< 10

而組合出來的combined的邏輯是:

x =&> expr1(x) expr2(x)

也就是直接調用expr1與expr2得到結果來做操作得到結果(注意這裡有短路求值語義喔,雖然這個例子中沒有副作用看不出來短路與否的區別)。

而"Invoke",函數調用,用lambda演算的角度來看,就是β-reduction——function application。

expr1(x) ,換個寫法就是 (a =&> a &>= 0)(x) ,經過β-reduction之後得到的就是 x &>= 0。

對 expr2(x) 如法炮製,(b =&> b &< 10)(x) 應用β-reduction得到 x &< 10。

把這倆放在combined中,就變成了:

x =&> (x &>= 0) (x &< 10)

可以看到,正如 @Ivony 大大的回答所說,這就是「把函數內聯」的效果。

題主所給出的示例代碼中,把expr1與expr2的函數體提取出來,並將它們的參數替換為combined的參數,這就是β-reduction。

2、如果我改寫成OrElse,這裡是不是有影響?

如果您需要的組合方式是 expr1(x) || expr2(x) 的話,那麼OrElse()就對了

3、如果想深入學習該方面的知識,有沒有推薦的書籍?

這個問題好難回答…其實都是一些編譯原理的基本原理的應用,混入一些lambda演算的皮毛。

LINQ Expression Tree / DLR Expression Tree就是一種比較典型的樹形/DAG的IR,對它的各種操作都是編譯器中端會涉及的知識,所以如果有一些編譯原理基礎的話學習起來就會很輕鬆。

不過沒有基礎也沒關係,就是棵表達程序語義的樹/DAG而已…

我以前寫過一些博客文章跟LINQ與DLR Expression Tree相關,如果有興趣也可以參考一下:RednaxelaFX的博客 - DLR分類文章列表 - ITeye技術網站

有本書介紹DLR(與LINQ Expression Tree)的特性和用法的,或許可以參考:Pro DLR in .NET 4


簡單說就是這樣,我們假定有兩個函數:

a =&> a &< 10 b =&> b &> 0

其中,a和b是

expr1.Parameters[0]

expr2.Parameters[0]

a &< 10 b &> 0

則是

expr1.Body

expr2.Body

所以,其實就是產生這樣一個東西:

x =&> //x 就是 parameter = Expression.Parameter(typeof (T));
x &< 10 //第一個函數,用x替換了a //AndAlso x &> 0 //第二個函數,用x替換了b

就是這個樣子:

x =&> x &< 10 x &> 0

所以簡單說,就是把函數內聯。

書籍我自己也想找人給我推薦,我啃DLR也很費勁,,,,


這就是看msdn能看懂的東西 沒必要看書啦。。。


原理前面幾位大神都已經說的很清楚了 書籍的話我推薦c# in deepth 裡面有講到lambada 怎麼玩兒

另外裡面用到了設計模式裡面一個叫做訪問者模式的東西 expressionvisiter會替你去遍歷表達式(表達式本質上是一顆二叉樹) 在遍歷的時候你的代碼等於說是把expr1和expr2表達式的參數統一了起來 (Var parameter)這樣兩者才可以andalso 不然他們兩個因為是兩個不同的實例所以其實是沒法放在一起的

建議可以搜索訪問者模式以及expression Visitor這個類的用法。。。。最後還有c# in deepth這本書

最後表示linq lambda真的很好玩兒 可以寫各種有趣的東西。。。


推薦閱讀:

在 ASP.NET Core 已經推出的今天,IIS 會被砍嗎?
老師說linq語句過時了,是真的嗎?
Entity Framework裡面 使用Code First 還是 Model First / Database First?
如何在C#中存儲大量數據而不引發OutOfMemoryException?
為什麼聽說過 JVM 調優,卻沒聽說過 CLR 調優?

TAG:NET | C# | 編譯原理 | linq |