Ngôn ngữ LINQ - Cây biểu thức trong LINQ
Cây biểu thức trong LINQ
Cây biểu thức (expression tree) là các biểu thức được sắp xếp trong cấu trúc dữ liệu giống như cây. Mỗi nút trong cây biểu thức là một biểu thức.
Ví dụ, một cây biểu thức có thể được sử dụng để biểu diễn công thức toán học x < y trong đó x
, <
và y
sẽ được biểu diễn dưới dạng một biểu thức và được sắp xếp trong cấu trúc giống như cây.
Cây biểu thức là một mô tả trong bộ nhớ của biểu thức lambda. Nó chứa các phần tử thực tế của truy vấn, không phải là kết quả của truy vấn.
Cây biểu thức làm cho cấu trúc của biểu thức lambda trong suốt và rõ ràng. Bạn có thể tương tác với dữ liệu trong cây biểu thức giống như với bất kỳ cấu trúc dữ liệu nào khác.
Ví dụ, hãy xem xét biểu thức isTeenAgerExpr sau đây:
Expression<Func<Student, bool>> isTeenAgerExpr = s => s.Age > 12 && s.Age < 20;
Trình biên dịch sẽ dịch biểu thức trên thành cây biểu thức sau:
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 });
Bạn cũng có thể xây dựng một cây biểu thức bằng tay. Hãy xem cách xây dựng cây biểu thức cho biểu thức lambda đơn giản sau:
Func<Student, bool> isAdult = s => s.age >= 18;
Delegate Func này tương đương với phương thức sau:
public bool IsAdult(Student s)
{
return s.Age >= 18;
}
Để tạo cây biểu thức, trước hết, hãy tạo một biểu thức tham số trong đó Student là kiểu tham số và 's' là tên của tham số như dưới đây:
ParameterExpression pe = Expression.Parameter(typeof(Student), "s");
Bước 1: Tạo biểu thức tham số trong LINQ
Bây giờ, sử dụng phương thức Expression.Property()
để tạo biểu thức s.Age trong đó s là tham số và Age là tên thuộc tính của Student. (Expression là một lớp trừu tượng có chứa các phương thức trợ giúp tĩnh để tạo cây biểu thức theo cách thủ công.)
MemberExpression me = Expression.Property(pe, "Age");
Bước 2: Tạo biểu thức thuộc tính trong LINQ
Bây giờ, tạo một biểu thức hằng cho 18 (tuổi):
ConstantExpression constant = Expression.Constant(18, typeof(int));
Bước 3: Tạo biểu thức hằng trong LINQ
Như vậy là chúng ta đã xây dựng được cây biểu thức cho s.Age (biểu thức thuộc tính) và 18 (biểu thức hằng).
Bây giờ chúng ta cần kiểm tra xem biểu thức thuộc tính có lớn hơn biểu thức hằng hay không.
Để làm điều này, chúng ta sẽ sử dụng phương thức Expression.GreaterThanOrEqual()
và truyền biểu thức thuộc tính, biểu thức hằng làm tham số như sau:
BinaryExpression body = Expression.GreaterThanOrEqual(me, constant);
Bước 4: Tạo biểu thức nhị phân trong LINQ
Vậy là chúng ta đã xây dựng xong cây biểu thức cho phần thân biểu thức lambda s.Age >= 18
.
Bây giờ chúng ta cần ghép nối các tham số và thân biểu thức lại. Sử dụng phương thức Expression.Lambda(body, parameters array)
để nối phần thân biểu thức và phần tham số của biểu thức lambda s => s.age >= 18
như sau:
var isAdultExprTree = Expression.Lambda<Func<Student, bool>>(body, new[] { pe });
Bước 5: Tạo biểu thức Lambda trong LINQ
Bằng cách này, bạn có thể xây dựng cây biểu thức cho các delegate Func đơn giản với biểu thức lambda.
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 });
Console.WriteLine("Expression Tree: {0}", expressionTree);
Console.WriteLine("Expression Tree Body: {0}", expressionTree.Body);
Console.WriteLine("Number of Parameters in Expression Tree: {0}",
expressionTree.Parameters.Count);
Console.WriteLine("Parameters in Expression Tree: {0}", expressionTree.Parameters[0]);
Ví dụ: Cây biểu thức trong LINQ
Đây là kết quả khi biên dịch và thực thi chương trình:
Expression Tree: s => (s.Age >= 18)
Expression Tree Body: (s.Age >= 18)
Number of Parameters in Expression Tree: 1
Parameters in Expression Tree: s
Hình ảnh sau đây minh họa toàn bộ quá trình tạo cây biểu thức:
Tại sao lại dùng cây biểu thức?
Chúng ta đã thấy trong phần trước rằng biểu thức lambda gán cho delegate Func<T> được biên dịch thành mã thực thi và biểu thức lambda gán cho Expression<TDelegate> được biên dịch vào cây biểu thức (expression tree).
Mã thực thi thực hiện trong cùng một miền ứng dụng để xử lý tập hợp trong bộ nhớ. Các lớp tĩnh có thể chứa các phương thức mở rộng cho các tập hợp trong bộ nhớ triển khai interface IEnumerable<T>, ví dụ: List<T>, Dictionary<T>, v.v.
Các phương thức mở rộng trong lớp Enumerable chấp nhận một tham số kiểu delegate Func. Ví dụ, phương thức mởi rộng Where chấp nhận tham số delegate Func<TSource, bool>.
Sau đó, nó biên dịch thành IL (Intermediate Language - Ngôn ngữ trung gian) để xử lý các tập hợp trong bộ nhớ trong cùng một miền ứng dụng (AppDomain).
Hình ảnh sau đây minh họa phương thức mở rộng trong lớp Enumerable có tham số là delegate Func:
Delegate Func là một mã thực thi thô, vì vậy nếu bạn Debug, bạn sẽ thấy rằng delegate Func sẽ được biểu diễn như hình dưới đây. Bạn không thể thấy các tham số, kiểu trả về và phần thân biểu thức của nó:
Delegate Func dành cho các tập hợp trong bộ nhớ vì nó sẽ được xử lý trong cùng một AppDomain, nhưng còn các trình cung cấp truy vấn LINQ từ xa như LINQ-to-SQL, EntityFramework hoặc các sản phẩm của bên thứ ba khác cung cấp khả năng LINQ thì sao?
Làm thế nào họ phân tích biểu thức lambda đã được biên dịch thành mã thực thi thô để biết về các tham số, trả về loại biểu thức lambda và xây dựng truy vấn thời gian chạy để xử lý thêm?
Câu trả lời là cây biểu thức.
Expression<TDelegate> được biên dịch thành cấu trúc dữ liệu gọi là cây biểu thức. Nếu bạn Debug, delegate Expression sẽ được trình bày như dưới đây:
Bây giờ bạn có thể thấy sự khác biệt giữa một delegate bình thường và Expression. Một cây biểu thức rất rõ ràng, bạn có thể truy xuất một tham số, kiểu trả về và thông tin thân biểu thức từ biểu thức, như sau:
Expression<Func<Student, bool>> isTeenAgerExpr = s => s.Age > 12 && s.Age < 20;
Console.WriteLine("Expression: {0}", isTeenAgerExpr );
Console.WriteLine("Expression Type: {0}", isTeenAgerExpr.NodeType);
var parameters = isTeenAgerExpr.Parameters;
foreach (var param in parameters)
{
Console.WriteLine("Parameter Name: {0}", param.Name);
Console.WriteLine("Parameter Type: {0}", param.Type.Name );
}
var bodyExpr = isTeenAgerExpr.Body as BinaryExpression;
Console.WriteLine("Left side of body expression: {0}", bodyExpr.Left);
Console.WriteLine("Binary Expression Type: {0}", bodyExpr.NodeType);
Console.WriteLine("Right side of body expression: {0}", bodyExpr.Right);
Console.WriteLine("Return Type: {0}", isTeenAgerExpr.ReturnType);
Đây là kết quả khi biên dịch và thực thi chương trình:
Expression: s => ((s.Age > 12) AndAlso (s.Age < 20))
Expression Type: Lambda
Parameter Name: s
Parameter Type: Student
Left side of body expression: (s.Age > 12)
Binary Expression Type: AndAlso
Right side of body expression: (s.Age < 20)
Return Type: System.Boolean
Truy vấn LINQ cho LINQ-to-SQL hoặc Entity Framework không được thực thi trong cùng một miền ứng dụng. Ví dụ: truy vấn LINQ sau đây cho Entity Framework thực sự không bao giờ được thực hiện bên trong chương trình của bạn:
var query = from s in dbContext.Students
where s.Age >= 18
select s;
Ví dụ: Truy vấn LINQ trong C#
Đầu tiên, nó được dịch thành một câu lệnh SQL và sau đó được thực thi trên máy chủ cơ sở dữ liệu.
Mã được tìm thấy trong một biểu thức truy vấn phải được dịch thành một truy vấn SQL có thể được gửi đến một tiến trình khác dưới dạng một chuỗi.
Đối với LINQ-to-SQL hoặc Entity Frameworks, tiến trình đó xảy ra trên một máy chủ cơ sở dữ liệu SQL.
Rõ ràng việc dịch cấu trúc dữ liệu như cây biểu thức sang SQL sẽ dễ dàng hơn nhiều so với dịch mã IL thô hoặc mã thực thi sang SQL bởi vì như bạn đã thấy, rất dễ lấy thông tin từ một biểu thức.
Cây biểu thức được tạo cho nhiệm vụ chuyển đổi mã, chẳng hạn như chuyển một biểu thức truy vấn thành một chuỗi có thể được truyền cho một số tiến trình khác và được thực hiện ở đó.
Lớp tĩnh Queryable bao gồm các phương thức mở rộng chấp nhận tham số vị ngữ kiểu Expression. Biểu thức vị ngữ này sẽ được chuyển đổi thành cây biểu thức và sau đó sẽ được chuyển đến trình cung cấp LINQ từ xa dưới dạng cấu trúc dữ liệu để nhà cung cấp có thể xây dựng một truy vấn thích hợp từ cây biểu thức và thực hiện truy vấn