1. Sự tiến hóa của nỗi đau
Chắc bạn từng gặp kiểu này: không chỉ cần duyệt qua collection mà còn phải biết số thứ tự của từng phần tử. Ví dụ để in ra danh sách có số thứ tự, đổi mấy phần tử có index khác 0, hoặc làm gì đó với mỗi phần tử thứ ba. Trong for truyền thống thì dễ òm:
// Quen thuộc, classic C#:
for (int i = 0; i < list.Count; i++)
{
Console.WriteLine($"{i}: {list[i]}");
}
Nhưng nếu bạn bắt đầu viết bằng LINQ thì tự nhiên... mất tiêu cái index! Mấy cái .Where, .Select, .OrderBy nhìn đẹp mà lại không cho bạn số thứ tự — chỉ có mỗi phần tử thôi. Tất nhiên ai cũng muốn làm kiểu này:
list.SelectWithIndex((item, index) => ...);
Nhưng mà không có method chuẩn nào tên SelectWithIndex cả. Dùng overload của Select thì cũng được, nhưng... nếu chỉ muốn lấy index mà không muốn transform hay project gì, lại phải chế thêm .Select phụ, làm code LINQ đẹp đẽ thành ra rối rắm.
Làm sao sống sót trước .NET 9
Ngày xưa dân C# cũng có mấy trick kiểu hacker:
var result = list.Select((item, index) => new { item, index });
Hoặc kết hợp với mấy method LINQ khác, nhưng lúc nào cũng thấy không "native", và không đúng kiểu đẹp như mong muốn.
2. Phương thức LINQ mới: Index — nó là gì và để làm gì?
Mô tả chính thức
Trong .NET 9, team dev đã nghe tiếng kêu than của hàng triệu coder (thôi thì, chục ngàn trên Twitter cũng đủ rồi) — và thêm vào LINQ một extension method mới — Index. Trên trang docs .NET 9 có ghi:
Index() thêm vào mỗi phần tử trong sequence số thứ tự của nó, bắt đầu từ 0, và trả về cặp (giá trị, index), không cần phải tự tạo anonymous object hay viết .Select((item, idx) => new { item, idx }) nữa.
Quá mạnh — giờ Index trả về cho bạn cặp: phần tử và index của nó.
Chữ ký method
public static IEnumerable<(T Element, int Index)> Index<T>(this IEnumerable<T> source);
Dịch cho dễ hiểu: Với mỗi phần tử trong collection, bạn nhận được một "tuple" đặc biệt — Element và Index của nó. Thế là xong. Không cần chế anonymous type nữa.
3. Ví dụ dùng Index
Ví dụ siêu đơn giản
Lấy thử danh sách trái cây yêu thích nhé.
var fruits = new List<string> { "Táo", "Chuối", "Cam", "Kiwi" };
foreach (var (fruit, idx) in fruits.Index())
{
Console.WriteLine($"{idx}: {fruit}");
}
Kết quả:
0: Táo
1: Chuối
2: Cam
3: Kiwi
Dễ như ăn kẹo! Giờ bạn có thể lấy cặp "index-giá trị" và dùng nó trong bất kỳ LINQ query nào.
Kết hợp với các LINQ method khác
Index là thành viên chính hiệu của gia đình LINQ! Dùng thoải mái trong "chuỗi" LINQ.
Ví dụ 1: Lọc phần tử có index lẻ
var numbers = Enumerable.Range(10, 10); // 10, 11, ... 19
var oddIndexes = numbers.Index()
.Where(pair => pair.Index % 2 == 1)
.Select(pair => pair.Element);
Console.WriteLine(string.Join(", ", oddIndexes));
Kết quả:
11, 13, 15, 17, 19
Ví dụ 2: Sửa phần tử có index chẵn
var users = new List<string> { "Anna", "Igor", "Katya", "Denis" };
var modified = users.Index()
.Select(pair => pair.Index % 2 == 0 ? pair.Element.ToUpper() : pair.Element.ToLower());
foreach (var name in modified)
Console.WriteLine(name);
Kết quả:
ANNA
igor
KATYA
denis
Ví dụ 3: Ghép với collection khác theo index (kiểu zip)
var ids = new[] { 101, 102, 103 };
var names = new[] { "Alice", "Bob", "Charlie" };
var merged = names.Index()
.Join(ids.Index(),
namePair => namePair.Index,
idPair => idPair.Index,
(namePair, idPair) => (idPair.Element, namePair.Element));
foreach (var (id, name) in merged)
Console.WriteLine($"{id}: {name}");
Kết quả:
101: Alice
102: Bob
103: Charlie
4. Index trong ứng dụng thực tế
Thử thêm vào app "học mãi" của chúng ta một chức năng mới: in ra danh sách tất cả sinh viên trong collection kèm số thứ tự (để user chọn sinh viên theo số).
Ví dụ: In danh sách sinh viên kèm số thứ tự
Giả sử đã có class Student từ ví dụ trước:
public class Student
{
public string Name { get; set; }
public int Grade { get; set; }
}
Tạo một danh sách sinh viên nhỏ:
var students = new List<Student>
{
new Student { Name = "Dasha", Grade = 5 },
new Student { Name = "Petya", Grade = 3 },
new Student { Name = "Vova", Grade = 4 },
new Student { Name = "Olya", Grade = 5 }
};
Giờ dùng Index để in đẹp kèm số thứ tự:
foreach (var (student, idx) in students.Index())
{
Console.WriteLine($"{idx + 1}. {student.Name} — Điểm: {student.Grade}");
}
Ở đây
idx + 1 — để đánh số từ 1 cho quen, không phải từ 0.
Kết quả:
1. Dasha — Điểm: 5
2. Petya — Điểm: 3
3. Vova — Điểm: 4
4. Olya — Điểm: 5
Lợi ích thực tế: Giờ user muốn chọn sinh viên theo số — xong luôn! Code gọn, không cần "đếm tay", dễ đọc — ít bug.
5. So sánh: Index ngon hơn cách cũ chỗ nào?
Trước .NET 9: kiểu cũ
Trước đây muốn lấy phần tử kèm index phải dùng overload .Select((item, index) => ...) và tự tạo anonymous type:
var withIndexes = students.Select((student, index) => new { student, index });
Muốn lấy field lại phải viết .student, .index — mà lại là anonymous type, không có tuple tên đẹp.
Với .NET 9: style thế kỷ XXI
Giờ — khỏi lo field, anonymous type. Mọi thứ rõ ràng:
foreach (var (student, idx) in students.Index())
{
// chạy luôn, trực quan, gọn, đẹp
}
Code sạch hơn. Ít code, ít lỗi. Quá hợp cho chuỗi LINQ dài, khỏi nghĩ về overload phụ.
6. Lưu ý và đặc điểm khi dùng
Phạm vi áp dụng
Index dùng được với mọi object implement IEnumerable<T>. Tức là — mọi collection thường, array, kết quả LINQ khác đều chơi được.
Kiểu trả về của Index?
Nó trả về enumeration các tuple, phần tử đầu là object (thường gọi Element), phần hai là Index (kiểu int). Nhờ cú pháp tuple hiện đại trong C#, bạn có thể viết luôn foreach (var (element, index) in ...) và lấy cả hai giá trị vào biến riêng.
Dùng được với Query Syntax không?
Không, Index là extension method, query syntax (LINQ kiểu SQL) không hỗ trợ trực tiếp. Tức là làm kiểu này không được:
// Không chạy!
var query = from s in students.Index() select ...;
Muốn kết hợp thì chỉ cần bọc method cần thiết trong ngoặc rồi dùng như collection bình thường:
var query = from pair in students.Index()
where pair.Index > 1
select pair.Element;
Index: luôn bắt đầu từ 0
Index luôn đếm từ 0, đúng kiểu lập trình C#. Nếu muốn bắt đầu từ 1 — chỉ cần cộng 1 ở chỗ cần thiết.
8. Lỗi và đặc điểm — cần chú ý gì
Nhiều bạn mới học hay nhầm giữa Index và overload .Select((item, index) => ...). Lỗi hay gặp:
— Thử dùng Index trong query syntax: "Sao không chạy?" — vì nó chỉ là extension method.
— Nghĩ index sẽ bắt đầu từ 1, nhưng thực ra là từ 0.
— Tưởng Index sẽ thay đổi collection gốc — nhưng như mọi LINQ method, nó trả về sequence mới, không sửa collection gốc (immutable-approach).
Một điểm thú vị nữa: nếu collection là "lười biếng" (nhớ nhé: LINQ mặc định là lazy), method Index cũng sẽ tính index khi truy cập phần tử, không phải tính trước. Rất hợp cho collection lớn hoặc thậm chí vô hạn — index luôn đúng và không "nổ RAM" của bạn đâu.
GO TO FULL VERSION