1. Giới thiệu
Trong các dự án thực tế gần như một nửa số lập trình viên dành phần lớn thời gian làm việc với collection: lọc, đếm, tìm kiếm, sắp xếp lại, thêm, lấy — nói chung là xử lý chúng gần như trước tủ lạnh vào ban đêm. Đặc biệt thường xuyên phải trích xuất, xử lý và tổng hợp dữ liệu — có thể là danh sách người dùng, sản phẩm trong catalog, các dòng text hoặc bất kỳ mảng nào khác.
Gần như tất cả collection hiện đại trong .NET hỗ trợ các phương thức theo phong cách functional — như Where, Select, Find, Any, All và nhiều cái khác. Sức mạnh của chúng nằm ở tính tổng quát và phong cách ngắn gọn: bạn đơn giản truyền một "mẩu logic" dưới dạng biểu thức lambda, và collection sống dậy như thể bạn đã lắp động cơ mới.
LINQ (Language Integrated Query) — không chỉ là syntactic sugar, mà là cả một mini-ngôn ngữ trong C#, cho phép viết truy vấn dữ liệu như thể bạn dùng SQL hoặc Excel. Chỉ là tốt hơn: ngay trong code, với autocomplete, kiểu dữ liệu và debugger.
Nhưng tất cả phép màu đó hoạt động nhờ delegate — và viết ra một method riêng cho mỗi bộ lọc thì khá mệt. Đây là lúc biểu thức lambda xuất hiện như các hàm nhỏ "tại chỗ", biến code cồng kềnh thành gọn gàng và biểu cảm.
2. Biểu thức lambda trong các method chuẩn của collection
Biểu thức lambda thể hiện tốt trong các method chuẩn của collection dựa trên delegate, như Find, Exists, ForEach và nhiều method khác.
Ví dụ: Tìm theo điều kiện
Giả sử bạn có một danh sách sản phẩm:
using System;
using System.Collections.Generic;
// Lớp Product của chúng ta
public class Product
{
public string Name { get; set; }
public int Price { get; set; }
}
var products = new List<Product>
{
new Product { Name = "Cà phê", Price = 100 },
new Product { Name = "Trà", Price = 70 },
new Product { Name = "Sữa", Price = 80 }
};
// Tìm sản phẩm đắt đầu tiên (>90)
Product expensive = products.Find(p => p.Price > 90); // Dùng lambda!
Console.WriteLine(expensive?.Name); // => Cà phê
Không có lambda bạn sẽ phải viết method riêng hoặc anonymous method kiểu cũ. Với lambda — một dòng, và code đọc như tiếng Anh: "Tìm sản phẩm mà giá lớn hơn 90".
Ví dụ: Kiểm tra tồn tại sản phẩm
bool hasCheap = products.Exists(p => p.Price < 75);
Console.WriteLine(hasCheap); // => True (vì "Trà" rẻ hơn 75)
Ví dụ: Xử lý mọi phần tử (ForEach)
Đôi khi cần làm gì đó với mỗi phần tử:
products.ForEach(p => Console.WriteLine($"{p.Name}: {p.Price} euro"));
Về ẩn dụ
Nói ngắn gọn: biểu thức lambda trong collection giống như nút "làm đẹp" trong trình chỉnh ảnh. Nhấn — và có kết quả!
3. Biểu thức lambda và LINQ: phép màu cho collection
LINQ — không chỉ tiện lợi, mà còn là bước vào lập trình functional. Hầu hết method của LINQ nhận delegate — nên bạn thân thiết với lambda expressions.
Lọc với Where
Giả sử lại danh sách products. Bây giờ lấy chỉ các hàng "rẻ":
using System.Linq;
var cheapProducts = products.Where(p => p.Price < 90);
foreach (var p in cheapProducts)
Console.WriteLine(p.Name); // Trà, Sữa
Ta có một collection mới mà không cần vòng lặp thủ công. Where nhận một predicate lambda (hàm trả về true/false) và áp dụng cho từng phần tử.
Sắp xếp với OrderBy
Ai thích thứ tự — vào đây:
var sorted = products.OrderBy(p => p.Price);
foreach (var p in sorted)
Console.WriteLine($"{p.Name}: {p.Price}");
// Trà: 70
// Sữa: 80
// Cà phê: 100
Mapping (Select) – projection dữ liệu
Đôi khi cần không phải toàn bộ object mà chỉ một phần, ví dụ danh sách tên sản phẩm:
var names = products.Select(p => p.Name);
foreach (var name in names)
Console.WriteLine(name); // Cà phê, Trà, Sữa
Chuỗi LINQ
LINQ hay ở chỗ có thể "xâu" các gọi liên tiếp:
var namesOfCheap = products
.Where(p => p.Price < 90)
.OrderBy(p => p.Name)
.Select(p => p.Name.ToUpper());
foreach (var name in namesOfCheap)
Console.WriteLine(name); // SỮA, TRÀ
Giống như dây chuyền sản xuất: mỗi method là một bước xử lý mới.
Câu hỏi: Tại sao lambda tốt hơn method bình thường cho LINQ?
Thứ nhất, lambda viết ngay chỗ cần. Thứ hai, lambda ngắn và dễ đọc. Thứ ba, đó là tiêu chuẩn của C# hiện đại — ai không dùng thường khó qua phỏng vấn.
4. Ví dụ thực tế
Trong khóa học ta đã viết một app học tập để làm việc với catalog nhỏ gồm sản phẩm, người dùng hoặc đơn hàng. Giờ thêm các phương thức xử lý collection hiện đại vào.
Tìm user theo tên
public class User
{
public string Username { get; set; }
public int Age { get; set; }
}
var users = new List<User>
{
new User{ Username = "Alice", Age = 21 },
new User{ Username = "Bob", Age = 26 },
new User{ Username = "Charlie", Age = 32 }
};
// Tìm user theo tên
User found = users.FirstOrDefault(u => u.Username == "Bob");
Console.WriteLine(found?.Age); // 26
Lọc theo tuổi
var adults = users.Where(u => u.Age >= 18);
foreach (var u in adults)
Console.WriteLine(u.Username); // Alice, Bob, Charlie
Đếm số user
int count = users.Count(u => u.Age > 25);
Console.WriteLine(count); // 2 (Bob và Charlie)
Kiểm tra tất cả user đều đủ tuổi trưởng thành
bool allAdults = users.All(u => u.Age >= 18);
Console.WriteLine(allAdults); // True
Có user vị thành niên không?
bool hasMinor = users.Any(u => u.Age < 18);
Console.WriteLine(hasMinor); // False
5. LINQ: Hoạt động phía trong như thế nào
Khi bạn viết, ví dụ, Where(u => u.Age > 20), thực tế gần như tương đương với việc tạo một vòng lặp duyệt tất cả phần tử và kiểm tra điều kiện cho mỗi phần tử. Chỉ là LINQ làm việc đó kín đáo và trông đẹp, đóng gói predicate của bạn vào delegate.
Bởi nếu không có biểu thức lambda, bạn sẽ phải viết mấy thứ kiểu:
public static bool AgeMoreThan20(User u) => u.Age > 20;
var adultUsers = users.Where(AgeMoreThan20);
Hoặc anonymous method theo style "cũ":
var adultUsers = users.Where(delegate(User u) { return u.Age > 20; });
Tất cả trông cồng kềnh và chán. Với lambda — lịch sự và hiện đại.
6. Delegate và các type chuẩn: Func, Action, Predicate
Không chỉ LINQ mê lambda. Nhiều method của collection chuẩn nhận các delegate chuyên dụng, ví dụ:
- Predicate<T> — cho các method Find, Exists, RemoveAll
- Func<T, TResult> — cho LINQ-methods, projection, tính toán
- Action<T> — cho method làm gì đó với element mà không trả về gì (ForEach)
Ví dụ thực tế như sau:
// Predicate<T>
users.RemoveAll(u => u.Age < 30); // Xóa tất cả trẻ hơn 30
// Func<T, TResult>
var names = users.Select(u => u.Username);
// Action<T>
users.ForEach(u => Console.WriteLine(u.Username));
7. Cheat-sheet các method collection với biểu thức lambda
| Method | Làm gì | Kiểu delegate | Ví dụ lambda |
|---|---|---|---|
|
Lọc phần tử | |
|
|
Chiếu, chuyển đổi | |
|
|
Sắp xếp theo key | |
|
|
Phần tử đầu theo điều kiện | |
|
|
Có ít nhất một phần tử thỏa điều kiện không | |
|
|
Tất cả phần tử có thỏa điều kiện không | |
|
|
Số phần tử theo điều kiện | |
|
|
Thực hiện hành động cho mỗi phần tử | |
|
|
Xóa tất cả theo predicate | |
|
8. Lỗi thường gặp và lưu ý
Một lỗi thường gặp là quên rằng LINQ không thay đổi collection gốc mà trả về một sequence mới. Tức là sau câu lệnh var sorted = users.OrderBy(u => u.Age); collection users vẫn giữ thứ tự ban đầu! Điều này dễ gây nhầm: đôi khi bạn tưởng là đã sắp xếp — nhưng thật ra chưa.
Một điểm nữa: các method như Where, Select trả về IEnumerable<T>. Đây là một collection "lazy" — việc xử lý thực sự chỉ bắt đầu khi bạn enumerate nó (foreach, ToList() v.v.). Nếu muốn materialize kết quả, nhớ gọi ToList() hoặc ToArray():
var sortedList = users.OrderBy(u => u.Age).ToList();
Còn nữa: nếu lambda capture biến ngoài scope (closures), thì những biến đó sẽ "sống" trong memory miễn là lambda còn sống. Không quá nghiêm trọng, nhưng nếu bạn dùng lambda trong object sống lâu và bắt giữ một mảng lớn, mảng đó sẽ bị giữ ở memory cùng với lambda.
Và một lời khuyên: dùng tên tham số và biến có ý nghĩa — điều này tăng readability nhiều, nhất là khi có nhiều cấp lambda lồng nhau.
GO TO FULL VERSION