Viết bởi Ali Hamza Ansari, 23 tháng 9, 2025
Bạn đã mệt mỏi với những khối if-else bất tận chỉ để xây dựng truy vấn? Điều gì sẽ xảy ra nếu truy vấn LINQ của bạn có thể tự viết chính mình tại thời điểm chạy? Hôm nay, chúng ta sẽ khám phá cây biểu thức, có thể được sử dụng để tạo các truy vấn động tại thời điểm chạy. Tôi sẽ chỉ cách sử dụng cây biểu thức trong dự án của bạn và hiểu rõ ưu điểm cùng hạn chế của chúng.
Mục lục
Cây biểu thức là gì
Cây biểu thức là cấu trúc dữ liệu bất biến trong C# biểu diễn mã nguồn dưới dạng cấu trúc cây. Một cây biểu thức bao gồm các biểu thức như phép toán nhị phân, lời gọi phương thức, hoặc giá trị hằng số, mỗi phần tử được biểu diễn như một nút. Chúng cho phép bạn xây dựng mã và truy vấn LINQ tại thời điểm chạy và thực thi chúng dựa trên đầu vào người dùng.
Trường hợp sử dụng của cây biểu thức
Cây biểu thức cung cấp nhiều ứng dụng, giúp lập trình trở nên dễ dàng và mạnh mẽ hơn. Hãy xem qua một số trường hợp sử dụng phổ biến.
LINQ provider động: Entity Framework sử dụng cây biểu thức để phân tích các truy vấn LINQ phía sau hậu trường để dịch chúng thành câu lệnh SQL. Khi bạn viết truy vấn LINQ như context.Buildings.Where(b => b.Name == "Tower A"), LINQ provider kiểm tra cây biểu thức biểu diễn b => b.Name == "Tower A". Sau khi xác minh thành công cây biểu thức, truy vấn LINQ dịch nó thành truy vấn SQL (SELECT * FROM Buildings WHERE Name= "Tower A").
Tạo mã tại thời điểm chạy: Cây biểu thức có thể là công cụ xây dựng mã động của bạn. Bạn có thể định nghĩa các phương thức như lambda cây biểu thức, sau đó biên dịch chúng thành delegate.
Công cụ Meta-Programming: Vì cây biểu thức truy cập runtime, chúng có thể là công cụ cho các kịch bản meta-programming nơi bạn có thể kiểm tra cấu trúc mã. Nếu bạn muốn biết thêm về meta-programming với reflection, hãy đọc bài viết khác của tôi 4 ví dụ thực tế sử dụng reflection trong C#.
Xây dựng Truy Vấn LINQ Động: Trong khi truy vấn LINQ là một sự tiện lợi hơn so với viết SQL, bạn có thể đi xa hơn một bước với cây biểu thức. Với sự trợ giúp của biểu thức predicate, bạn có thể tạo các truy vấn LINQ động cho các điều kiện khác nhau tại thời điểm chạy. Điều này cung cấp một lối thoát khỏi nhu cầu mã phức tạp cho nhiều điều kiện và hữu ích khi xây dựng bộ lọc tìm kiếm hoặc truy vấn phức tạp dựa trên đầu vào động từ người dùng.
Ví dụ: Mã thời gian chạy cho phép toán cộng cơ bản
Hãy hiểu cây biểu thức bằng ví dụ. Tôi sẽ xây dựng mã để thêm giá trị hằng số vào đầu vào người dùng và trả về kết quả.
using System.Linq.Expressions;
ParameterExpression param = Expression.Parameter(typeof(int), "x");
ConstantExpression constant = Expression.Constant(5);
BinaryExpression body = Expression.Add(param, constant);
Expression<Func<int, int>> lambda = Expression.Lambda<Func<int, int>>(body, param);
var compiledLambda = lambda.Compile();
int result = compiledLambda(10);
Console.WriteLine(result);
Như chúng ta biết, mỗi biểu thức là một đoạn mã tạo ra một giá trị.
Biểu thức có thể là hằng số hoặc biến, hoặc lời gọi phương thức phức tạp với các phép toán. Mỗi bước được định nghĩa như một biểu thức. ParameterExpression đại diện cho tham số đầu vào tại thời điểm chạy, chỉ định kiểu là int và tên là x. Tôi khởi tạo ConstantExpression cho giá trị cứng sẽ sử dụng sau trong mã. BinaryExpression tạo một nút phép toán nhị phân nơi tôi thêm tham số với hằng số sử dụng Expression.Add. Expression<Func<int, int>> trong lambda = Expression.Lambda<Func<int, int>>(body, param) tạo cây biểu thức lambda được kiểu mạnh phù hợp với delegate Func<int, int>. Delegate Func chỉ định kiểu đầu vào và đầu ra theo quy tắc Func<TInput, TOutput>. Cuối cùng, lambda.Compile() chuyển đổi cây biểu thức thành mã IL có thể thực thi. Chúng ta có thể tái sử dụng mã đã biên dịch với compiledLambda(10).
Ví dụ: Lọc tòa nhà với trường động
Trong ví dụ tiếp theo này, chúng tôi muốn cho phép người dùng lọc tòa nhà tại thời điểm chạy bằng bất kỳ thuộc tính nào họ chọn. Chúng tôi có một tòa nhà với các thuộc tính sau:
public class Building
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public int Floors { get; set; }
public string City { get; set; } = string.Empty;
public int YearBuilt { get; set; }
}
Bây giờ, chúng tôi cần một truy vấn LINQ động cho phép người dùng lọc theo bất kỳ thuộc tính nào tại thời điểm chạy. Áp dụng điều đó là khó khăn và ít bảo trì với LINQ tĩnh. Đây là lúc cây biểu thức trợ giúp.
var buildings = new List<Building>
{
new Building { Id = 1, Name = "Tower One", Floors = 10, City = "Karachi", YearBuilt = 1990 },
new Building { Id = 2, Name = "Skyline Plaza", Floors = 25, City = "Lahore", YearBuilt = 2005 },
new Building { Id = 3, Name = "Heritage Hall", Floors = 5, City = "Karachi", YearBuilt = 1950 }
};
var propertyName = "City"; // có thể là "Floors", "YearBuilt", v.v.
object filterValue = "Karachi"; // có thể là 10, 2005, v.v.
var parameter = Expression.Parameter(typeof(Building), "b");
var property = Expression.Property(parameter, propertyName);
var constant = Expression.Constant(filterValue);
var body = Expression.Equal(property, constant);
var lambda =
Expression.Lambda<Func<Building, bool>>(body, parameter);
// Sử dụng lambda động trong LINQ
var result = buildings.AsQueryable().Where(lambda);
foreach (var building in result)
{
Console.WriteLine($"{building.Name} - {building.City}");
}
Đầu tiên, tôi tạo danh sách tòa nhà để giữ mọi thứ đơn giản. Sau đó tôi giả định giá trị đầu vào như các biến propertyName và filterValue. Tôi định nghĩa một tham số như kiểu Building và đặt tên là b. Sau đó Expression.Property chỉ định thuộc tính đầu vào trong tham số sẽ được sử dụng trong bộ lọc. Tiếp theo, filterValue được định nghĩa như một hằng số. Hoạt động thực tế nằm trong phần thân xác định so sánh giữa giá trị đầu vào và thuộc tính đã chọn với Expression.Equal. Cuối cùng, một biểu thức lambda được định nghĩa phù hợp với delegate có Building như đầu vào và bool như đầu ra. Trong phần kết quả, lambda được sử dụng như một bộ lọc bên trong phương thức Where.
Bây giờ thay đổi thuộc tính đầu vào và giá trị lọc:
var propertyName = "Floors";
object filterValue = 25;
Do đó, chúng ta chỉ cần thay đổi propertyName và filterValue thành ‘Floors’ và 25 tương ứng, giống như đầu vào người dùng sẽ có.
Lợi ích của cây biểu thức
- Trình xây dựng truy vấn tùy chỉnh: Bạn có thể sử dụng cây biểu thức để xây dựng trình xây dựng truy vấn tùy chỉnh tạo các truy vấn tìm kiếm phức tạp dựa trên điều kiện động hoặc kịch bản kinh doanh cụ thể.
- Giảm độ phức tạp mã: Bằng cách sử dụng truy vấn LINQ động, bạn có thể tránh viết mã phức tạp, khó bảo trì được yêu cầu để xây dựng các truy vấn động sử dụng cây biểu thức hoặc kỹ thuật khác.
- Tăng khả năng đọc và bảo trì: Truy vấn LINQ động thường dẫn đến mã dễ đọc hơn, vì bạn có thể sử dụng cú pháp truy vấn quen thuộc thay vì xử lý sự phức tạp của cây biểu thức.
- Dễ sử dụng: Thư viện Dynamic LINQ đơn giản hóa quá trình xây dựng truy vấn động, giúp các nhà phát triển dễ dàng triển khai tạo truy vấn thời gian chạy.
Hạn chế của cây biểu thức
Mặc dù cung cấp tính linh hoạt trong truy vấn và mã, cây biểu thức có một số hạn chế.
- Không thể biểu diễn mọi tính năng C#: Cây biểu thức không hỗ trợ một số cấu trúc mã, như vòng lặp, câu lệnh goto, biến cục bộ (khai báo và gán lại bên trong cây), tuple literals, và so sánh tuple, cũng như khối try/catch/finally. Hơn nữa, bạn không thể sử dụng Interpolated Strings hoặc chuyển đổi chuỗi UTF-8.
- Cấu trúc chỉ đọc: Cây bất biến có nghĩa một khi đã tạo, bạn không thể “thay đổi” một nút. Bạn phải xây dựng lại cây nếu muốn sửa đổi.
- Chi phí hiệu suất:
lambda.Compile()gây ra chi phí biên dịch. Thực thi các delegate đã biên dịch nhanh, nhưng thường chậm hơn một chút so với phương thức C# thuần. - Dịch Entity Framework: Cây biểu thức được biên dịch với
Compile()không thể được dịch thành SQL bởi Entity Framework hoặc các LINQ provider khác. Chúng chỉ được đánh giá trong bộ nhớ. Sử dụng cây biểu thức không biên dịch trực tiếp với các provider khi bạn muốn thực thi phía máy chủ.
Mẹo sử dụng cây biểu thức hiệu quả nhất
Cây biểu thức mang lại lợi ích cho dự án của bạn theo nhiều cách. Nhưng bạn cần xem xét kịch bản tốt nhất để sử dụng.
- Sử dụng khi cần thiết: Cây biểu thức dài dòng và phức tạp. Không giống mã C# thuần, chúng liên quan đến khởi tạo hằng số, tham số, thuộc tính và biên dịch có thể khó bảo trì. Nếu truy vấn của bạn tĩnh và đã biết tại thời điểm biên dịch, hãy sử dụng LINQ thông thường và tránh overengineering mã.
- Cache lambda đã biên dịch: Như đã đề cập trước đó, biên dịch lambda cây biểu thức tốn kém. Cache delegate đã biên dịch và tái sử dụng nó:
var compiled = lambda.Compile();
_cache["MyLambda"] = compiled;
- Phân tách thành trình xây dựng nhỏ tái sử dụng: Sự đơn giản là chìa khóa cho bất kỳ mã nào. Tuân theo quy tắc ngón tay cái trong mã cây biểu thức. Phân tách một truy vấn phức tạp thành các phương thức nhỏ hơn, tái sử dụng:
public static Expression<Func<Building, bool>> FloorsGreaterThan(int minFloors)
{
var b = Expression.Parameter(typeof(Building), "b");
var property = Expression.Property(b, "Floors");
var constant = Expression.Constant(minFloors);
var body = Expression.GreaterThan(property, constant);
return Expression.Lambda<Func<Building, bool>>(body, b);
}
- Tận dụng thư viện hỗ trợ: Thay vì xây dựng cây bằng tay mỗi lần, sử dụng thư viện để tăng khả năng bảo trì. Thư viện
System.Linq.Dynamic.Coregiúp bạn viết truy vấn với chuỗi như"Price < 300"được chuyển đổi thành cây biểu thức.
Kết luận
Cây biểu thức trong .NET là một công cụ mạnh mẽ để thêm tính linh hoạt vào mã của bạn. Chúng cho phép xây dựng truy vấn LINQ tại thời điểm chạy và thực thi chúng dựa trên đầu vào người dùng. Nó phù hợp với kịch bản nơi các trường lọc không được định nghĩa và phụ thuộc vào người dùng. Tôi đã chia sẻ ví dụ về cách viết truy vấn và mã với cây biểu thức. Bằng cách làm theo các mẹo được thảo luận ở trên, bạn có thể tận dụng công cụ đáng chú ý này để làm cho mã mạnh mẽ và dễ bảo trì hơn.



