1. Giới thiệu
Lọc (filter) nghĩa là chọn ra từ collection chỉ những phần tử thỏa mãn điều kiện nào đó. Hình dung nhé: database của bạn có 10 000 sản phẩm, mà manager chỉ cần lấy đúng 12 cái phù hợp tiêu chí mới siêu dị. Viết vòng lặp thủ công và check từng cái thì vừa mệt vừa rối code. LINQ lo hết cho bạn rồi.
Trong app thực tế, filter là thao tác với data phổ biến nhất: chỉ show cho user những record họ quan tâm, "dọn dẹp" collection khỏi phần tử không mong muốn, lấy dữ liệu cho báo cáo, tìm kiếm hay gửi e-mail. Đi phỏng vấn, câu hỏi về LINQ filter gặp suốt luôn. Hiểu được chủ đề này là chìa khóa thành công cho dev .NET mới vào nghề.
2. Làm quen với method Where
Ý nghĩa và nguyên lý hoạt động
Method Where là extension method của LINQ, nhận vào một function (hoặc lambda expression) xác định điều kiện filter. Nó trả về một sequence mới (không làm thay đổi collection gốc!), chỉ chứa những phần tử mà điều kiện trả về true.
Signature cơ bản:
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate)
Đừng sợ generics hay mấy từ lạ hoắc. Điều quan trọng là hiểu ý tưởng: Where nhận collection gốc và điều kiện bạn đưa vào. Thực tế thì dễ lắm:
- source — collection bạn muốn filter (ví dụ: List<Product>)
- predicate — function/điều kiện (ví dụ: p => p.Price > 100)
Bài tập đơn giản khởi động
Giả sử có list sản phẩm, bạn muốn lấy ra những cái có giá lớn hơn 100. Ngày xưa chắc phải viết vòng lặp foreach rồi tự add phần tử vào list mới. LINQ cho bạn làm chuyện này chỉ với một dòng code.
3. Lọc dữ liệu
Class Product và collection khởi tạo
Ở module trước, mình có viết class sản phẩm kiểu như này:
class Product
{
public string Name { get; set; }
public double Price { get; set; }
// Cho đẹp khi in ra màn hình:
public override string ToString()
{
return $"{Name} (giá: {Price})";
}
}
Giả sử có list sản phẩm:
var products = new List<Product>
{
new Product { Name = "Bánh mì", Price = 30 },
new Product { Name = "Sữa", Price = 87 },
new Product { Name = "Phô mai", Price = 250 },
new Product { Name = "Sôcôla", Price = 130 }
};
Lọc sản phẩm giá trên 100 với Where
var expensiveProducts = products.Where(p => p.Price > 100);
foreach (var product in expensiveProducts)
{
Console.WriteLine(product);
}
Chuyện gì diễn ra bên trong:
- Where duyệt hết các phần tử trong list products,
- Với mỗi phần tử, nó gọi function (ở đây là: p => p.Price > 100),
- Nếu function trả về true, sản phẩm đó sẽ vào collection mới.
Kết quả trên màn hình:
Phô mai (giá: 250)
Sôcôla (giá: 130)
Lưu ý: list gốc products không bị thay đổi nhé! LINQ không phá dữ liệu của bạn đâu.
4. Lọc lười biếng (Deferred Execution)
Điểm hay và quan trọng: kết quả của Where chỉ được tính toán khi bạn thực sự truy cập phần tử. Ví dụ, khi bắt đầu vòng lặp foreach, LINQ mới "đi" qua collection gốc và filter từng phần tử ngay lúc đó. Nếu bạn không dùng kết quả, thì chẳng có gì xảy ra cả. Cái này gọi là thực thi trì hoãn (deferred execution).
Nó mang lại nhiều lợi ích:
- Có thể xâu chuỗi nhiều method (filter, sort, ...) và chỉ khi truy cập kết quả thật sự thì mọi thứ mới chạy.
- Tiết kiệm bộ nhớ: không tạo ra các collection trung gian không cần thiết.
- Nếu cần, có thể dừng giữa chừng hoặc "ngắt" sau khi tìm đủ số phần tử mong muốn.
Sơ đồ trực quan
Collection gốc —► Where (điều kiện) —► Chỉ duyệt phần tử cần thiết
[ x ] [ x ] [ + ] [ + ] —► p.Price > 100 —► [ + ] [ + ]
(x — phần tử không thỏa điều kiện, + — thỏa điều kiện)
5. Các kiểu cú pháp
Kiểu method mở rộng (Method Syntax)
Kiểu này mình đang dùng nè:
var result = products.Where(p => p.Price > 100);
Kiểu khác: Query Syntax
LINQ còn hỗ trợ cú pháp giống SQL:
var result = from p in products
where p.Price > 100
select p;
Kết quả — y chang luôn!
Chọn cú pháp nào là tùy bạn, nhưng method syntax (dùng dấu chấm) phổ biến hơn, nhất là khi xâu chuỗi nhiều method trong dự án thực tế.
6. Điều kiện filter phức tạp
Nhiều điều kiện
Bạn có thể dùng toán tử logic (&&, ||, !):
// Tìm tất cả sản phẩm đắt, trừ "Sôcôla"
var filtered = products.Where(
p => p.Price > 100 && p.Name != "Sôcôla"
);
Lọc theo chuỗi (substring, phân biệt hoa thường, Contains)
// Sản phẩm có chữ "l" trong tên
var lProducts = products.Where(p => p.Name.Contains("l"));
Lưu ý: Contains phân biệt hoa thường nhé! Nếu muốn "không phân biệt hoa thường", làm như này:
var lProducts = products.Where(p => p.Name
.ToLower().Contains("l")); // giờ thì cả "Sữa" và "Sôcôla" đều được chọn
Lọc theo nhiều collection
Giả sử bạn có hai list — sản phẩm và thanh toán. Có thể filter sản phẩm xuất hiện trong list thanh toán — nhưng nên dùng Any và Join, phần này sẽ nói sau. Chỉ cần biết: LINQ còn filter được kiểu phức tạp hơn nữa!
7. Một số lưu ý hay ho
Lọc lồng nhau và xâu chuỗi method
Bạn có thể nối nhiều bước filter, gọi Where nhiều lần liên tiếp:
var filtered = products
.Where(p => p.Price > 100)
.Where(p => p.Name.StartsWith("S"));
Nhưng tốt nhất là gộp điều kiện vào một Where dùng toán tử logic — vừa hiệu quả vừa dễ đọc code.
So sánh built-in và function tự viết
Đôi khi toán tử chuẩn không đủ. Ví dụ, cần filter chuỗi không phân biệt hoa thường và theo culture. Lúc này nên dùng các method so sánh:
var rusProducts = products.Where(
p => p.Name.StartsWith("s", StringComparison.OrdinalIgnoreCase)
);
8. Lọc và input từ user
Thử làm filter đơn giản kiểu command line nhé! Ví dụ, hỏi user giá tối thiểu, rồi show ra các sản phẩm phù hợp.
Console.Write("Giá tối thiểu của sản phẩm? ");
var minPriceStr = Console.ReadLine();
if (double.TryParse(minPriceStr, out double minPrice))
{
var filtered = products.Where(p => p.Price >= minPrice);
foreach (var product in filtered)
{
Console.WriteLine(product);
}
}
else
{
Console.WriteLine("Lỗi: nhập không phải số.");
}
Giờ "mini-app" của bạn nhìn cũng giống shop online rồi đó!
9. Lỗi phổ biến và bẫy khi dùng Where
Lập trình cũng như đời, có nhiều bẫy lắm. Đây là mấy cái nên tránh.
Quên gọi ToList() hoặc ToArray()
Kết quả của Where — không phải list hay array đâu nhé! Nó là object IEnumerable<Product>. Thường thì chạy ngon trong vòng lặp foreach, nhưng nếu bạn cần collection (ví dụ: muốn truy cập theo index), nhớ gọi .ToList() hoặc .ToArray():
var filteredList = products.Where(p => p.Price > 100).ToList();
Nếu quên vụ này, dễ dính lỗi kiểu "Collection was modified during enumeration" — nhất là khi bạn thay đổi collection gốc trong lúc duyệt.
Thay đổi collection gốc
Vì LINQ filter là deferred, nếu bạn xóa phần tử khỏi list gốc trong lúc duyệt, có thể bị exception. Đặc biệt là khi chạy đa luồng hoặc collection bị thay đổi ngay lúc filter.
Null và predicate
Nếu collection có null, mà trong predicate bạn truy cập field của object mà không check, sẽ bị NullReferenceException. Ví dụ:
var filtered = products.Where(p => p.Name.StartsWith("A"));
Nếu có phần tử nào trong products là null, code sẽ crash.
Khuyến nghị: luôn check null nếu có nguy cơ có phần tử null:
var filtered = products.Where(p => p != null && p.Name.StartsWith("A"));
GO TO FULL VERSION