1. 程式人生 > 其它 >LINQ查漏補缺之Expression與ExpressionTree

LINQ查漏補缺之Expression與ExpressionTree

Expression與ExpressionTree

LINQ 表示式(Expression)

可以將lambda表示式分配給Func或Action型別委託,以處理記憶體中的集合。.NET編譯器在編譯時將分配給Func或Action型別委託的lambda表示式轉換為可執行程式碼。

LINQ引入了一種名為Expression的新型別,該型別代表強型別的lambda表示式。這意味著lambda表示式也可以分配給Expression 型別。.NET編譯器將分配給Expression 的lambda表示式轉換為Expression樹,而不是可執行程式碼。遠端LINQ查詢提供程式使用此表示式樹作為資料結構,以其構建執行時查詢(例如LINQ-to-SQL,EntityFramework或實現IQueryable 介面的任何其他LINQ查詢提供程式)。

lambda表示式分配給Func或Action委託與LINQ中的Expression時的區別:

Fuc = lambda expression -> Compliles into -> Excutable code

Expression = lambda expression -> Compliees into -> Expression Tree -> Used as a data structure by LINQ Provider

定義 Expression

引用System.Linq.Expressions名稱空間,並使用Expression 類定義一個Expression。Expression 需要委託型別Func或Action。

例如,你可以將 lambda 表示式賦給 Func 型別委託 的 isTeenAger 變數,如下所示:

示例:在C#中為表示式定義Func委託

public class Student 
{
    public int StudentID { get; set; }
    public string StudentName { get; set; }
    public int Age { get; set; }
}
Func<Student, bool> isTeenAger = s => s.Age > 12 && s.Age < 20;

現在,您可以使用Expresson包裝Func委託,將上述Func型別委託轉換為Expression,如下所示:

示例:在C#中定義表示式 Expresson

Expression<Func<Student, bool>> isTeenAgerExpr = s => s.Age > 12 && s.Age < 20;

以相同的方式,如果您不從委託返回值,則還可以用Expression包裝Action 型別委託。

示例:在C#中定義表示式

Expression<Action<Student>> printStudentName = s => Console.WriteLine(s.StudentName);

因此,您可以定義Expression 型別。現在,讓我們看看如何呼叫由Expression 包裝的委託。

呼叫表示式(Expression)

您可以用與委託相同的方式呼叫由Expression包裹的委託,但是首先需要使用Compile()方法進行編譯。Compile()返回FuncAction型別的委託,以便您可以像委託一樣呼叫它。

示例:在C#中呼叫表示式

Expression<Func<Student, bool>> isTeenAgerExpr = s => s.Age > 12 && s.Age < 20;

//使用Compile方法編譯Expression以將其作為委託呼叫
Func<Student, bool>  isTeenAger = isTeenAgerExpr.Compile();         
//Invoke
bool result = isTeenAger(new Student(){ StudentID = 1, StudentName = "Steve", Age = 20});

LINQ 表示式樹

顧名思義,表示式樹不過是按樹狀資料結構排列的表示式。表示式樹中的每個節點都是一個表示式。例如,表示式樹可用於表示數學公式x < y,其中x,< y 將表示為表示式,並排列在樹狀結構中。

表示式樹是lambda表示式的記憶體表示形式。它儲存查詢的實際元素,而不是查詢的結果。

表示式樹使lambda表示式的結構透明和顯式。您可以與表示式樹中的資料進行互動,就像與其他任何資料結構一樣。

例如,看以下isTeenAgerExpr表示式:

示例:用C#表示式

Expression<Func<Student, bool>> isTeenAgerExpr = s => s.age > 12 && s.age < 20;

編譯器會將上面的表示式轉換為以下表達式樹:

示例:C#中的表示式樹

Expression.Lambda<Func<Student, bool>>(
                Expression.AndAlso(
                    Expression.GreaterThan(Expression.Property(pe, "Age"), Expression.Constant(12, typeof(int))),
                    Expression.LessThan(Expression.Property(pe, "Age"), Expression.Constant(20, typeof(int)))),
                        new[] { pe });

您也可以手動構建表示式樹。讓我們看看如何為以下簡單的lambda表示式構建表示式樹:

示例:C#中的Func委託:

Func<Student, bool> isAdult = s => s.age >= 18;

此Func型別委託將被視為以下方法:

C#:

public bool function(Student s)
{
  return s.Age > 18;
}

要建立表示式樹,首先,建立一個引數表示式,其中Student是引數的型別,'s'是引數的名稱,如下所示:

步驟1:在C#中建立引數表示式

ParameterExpression pe = Expression.Parameter(typeof(Student), "s");

現在,使用Expression.Property()建立s.Age表示式,其中s是引數,Age是Student的屬性名稱。(Expression是一個抽象類,其中包含用於手動建立表示式樹的靜態幫助器方法。)

步驟2:在C#中建立屬性表示式

MemberExpression me = Expression.Property(pe, "Age");

現在,為18建立一個常量表達式:

步驟3:在C#中建立常量表達式

ConstantExpression constant = Expression.Constant(18, typeof(int));

到目前為止,我們已經為s.Age(成員表示式)和18(常量表達式)構建了表示式樹。現在,我們需要檢查成員表示式是否大於常量表達式。為此,請使用Expression.GreaterThanOrEqual() 方法,並將成員表示式和常量表達式作為引數傳遞::

步驟4:在C#中建立二進位制表示式

BinaryExpression body = Expression.GreaterThanOrEqual(me, constant);

因此,我們為lambda表示式主體 s.Age> = 18 構建了一個表示式樹。我們現在需要將引數表示式和主體表達式連線起來。使用Expression.Lambda(body,parameters array)連線lambda表示式s => s.age> = 18的body(主體)和parameter(引數)部分:

步驟5:在C#中建立Lambda表示式

var isAdultExprTree = Expression.Lambda<Func<Student, bool>>(body, new[] { pe });

這樣,您可以為帶有lambda表示式的簡單Func委託構建表示式樹。

示例:C#中的表示式樹

ParameterExpression pe = Expression.Parameter(typeof(Student), "s");
MemberExpression me = Expression.Property(pe, "Age");
ConstantExpression ce = Expression.Constant(18, typeof(int));
BinaryExpression body = Expression.GreaterThanOrEqual(me, ce);

var ExpressionTree = Expression.Lambda<Func<Student, bool>>(body, new[] { pe });
// Func<Student, bool> isAdult = s => s.Age >= 18;
// s: ParameterExpression
// s.Age: MemberExpression
// 18: ConstantExpression
// s.Age >= 18: Body as Binary Expression
// Func<Student, bool>: Expression Type
System.Console.WriteLine($"Expression Tree: {ExpressionTree}");
System.Console.WriteLine($"Expression Tree Body: {ExpressionTree.Body}");
System.Console.WriteLine($"Expression Tree's parameters count: {ExpressionTree.Parameters.Count}");
System.Console.WriteLine($"Expression Tree's first parameter: {ExpressionTree.Parameters[0]}");

輸出:

Expression Tree: s => (s.Age >= 18)
Expression Tree Body: (s.Age >= 18)  
Expression Tree's parameters count: 1
Expression Tree's first parameter: s 

為什麼選擇表達樹?

在Expression的講述中,我們已經看到分配給lambda表示式Func編譯為可執行程式碼,分配給lambda表示式Expression型別編譯為Expression樹。

可執行程式碼在同一個應用程式域中執行,以處理記憶體中的集合。可列舉的靜態類包含用於實現IEnumerable 介面的記憶體中集合的擴充套件方法,例如List ,Dictionary 等。Enumerable類中的擴充套件方法接受Func型別委託的謂詞引數。例如,Where擴充套件方法接受Func <TSource,bool>謂詞。然後,將其編譯為IL(中間語言)以處理同一AppDomain中的記憶體中集合。

Func委託是原始的可執行程式碼,因此,如果除錯程式碼,則會發現Func委託將表示為不透明程式碼。您無法看到其引數,返回型別和主體。

Func委託用於記憶體中的集合,因為它將在同一個AppDomain中進行處理,但是諸如LINQ-to-SQL,EntityFramework或其他提供LINQ功能的第三方產品的遠端LINQ查詢提供者呢?他們將如何解析已編譯為原始可執行程式碼的lambda表示式,以瞭解引數,lambda表示式的返回型別以及構建執行時查詢以進一步處理?答案是表達樹

Expression 被編譯成稱為表示式樹的資料結構。

表示式樹是透明的。您可以從表示式中檢索引數,返回型別和主體表達式資訊,如下所示:

示例:C#中的表示式樹

Expression<Func<Student, bool>> isTeenAgerExpr = s => s.Age > 12 && s.Age < 20;
Console.WriteLine("Expression: {0}", isTeenAgerExpr );        
Console.WriteLine("表示式型別: {0}", isTeenAgerExpr.NodeType);

var parameters = isTeenAgerExpr.Parameters;
foreach (var param in parameters)
{
    Console.WriteLine("引數名稱: {0}", param.Name);
    Console.WriteLine("引數型別: {0}", param.Type.Name );
}
var bodyExpr = isTeenAgerExpr.Body as BinaryExpression;
Console.WriteLine("表示式主體左側: {0}", bodyExpr.Left);
Console.WriteLine("二進位制表示式型別: {0}", bodyExpr.NodeType);
Console.WriteLine("表示式主體右側: {0}", bodyExpr.Right);
Console.WriteLine("返回型別: {0}", isTeenAgerExpr.ReturnType);

輸出:

Expression: s => ((s.Age > 12) AndAlso (s.Age < 20))
表示式型別: Lambda
引數名稱: s
引數型別: Student
表示式主體左側: (s.Age > 12)
二進位制表示式型別: AndAlso
表示式主體右側: (s.Age < 20)
返回型別: System.Boolean

不在同一應用程式域中執行鍼對LINQ-to-SQL或Entity Framework的LINQ查詢。例如,對於Entity Framework的以下LINQ查詢永遠不會在程式內部實際執行:

示例:C#中的LINQ查詢

var query = from s in dbContext.Students
            where s.Age >= 18
            select s;

首先將其轉換為SQL語句,然後在資料庫伺服器上執行。

在查詢表示式中找到的程式碼必須轉換為SQL查詢,該查詢可以作為字串傳送到另一個程序。對於LINQ-to-SQL或Entity Framework,該過程恰好是SQL Server資料庫。將資料結構(如表示式樹)轉換為SQL顯然比將原始IL或可執行程式碼轉換為SQL容易得多,因為正如您看到的,從表示式中檢索資訊很容易。

建立表示式樹的目的是將諸如查詢表示式之類的程式碼轉換為可以傳遞給其他程序並在此處執行的字串。

可查詢的靜態類包括接受Expression型別的謂詞引數的擴充套件方法。將該謂詞表達式轉換為表示式樹,然後將其作為資料結構傳遞到遠端LINQ提供程式,以便提供程式可以從表示式樹構建適當的查詢並執行查詢。