1. Giới thiệu
Lập trình giống như xếp đồ vào vali: thường thì bạn cần bỏ vào không chỉ một thứ mà là vài thứ cùng lúc. Ví dụ như vớ và sạc điện thoại. Mà đâu ai muốn mua nguyên cái vali-class chỉ để đựng tạm hai ba món (về class thì tụi mình sẽ học sau nha). Đôi khi chỉ cần trả về hai hoặc ba giá trị từ function thôi. Vậy phải làm sao?
Có vài cách kiểu "ông bà xưa" hay dùng. Ví dụ, trả về một mảng (int[] arr = {a, b};) hoặc dùng field của class chỉ để phục vụ một function. Cũng có mấy cách phức tạp hơn, tụi mình sẽ học ở bài sau (out-parameter hoặc truyền qua biến global). Nhưng mà ai cũng muốn làm ngay, làm cho gọn, hiện đại đúng không!
Vậy nên từ C# 7 có Value Tuple (Value Tuple): cú pháp siêu tiện để gom tạm nhiều giá trị khác kiểu thành một nhóm, khỏi phải lăn tăn về kiểu dữ liệu riêng. Vừa tiện, vừa làm code nhìn xịn hẳn ra.
2. Làm quen với tuple (Value Tuples)
Tuple giống như phép màu của dân code, cho phép nhét nhiều giá trị khác nhau vào một "túi" và mang đi cùng nhau. Hãy nghĩ nó như mấy tờ note dán nhanh — khỏi cần tạo folder riêng cho từng chuyện nhỏ!
Value Tuple (System.ValueTuple) — là một struct "tắc kè", chứa từ hai đến tám phần tử, kiểu gì cũng được. Muốn nhét số, string, bool chung? Ok luôn! Tuple không kén chọn đâu.
Ví dụ tạo tuple với nhiều kiểu khác nhau
// tuple — đây là (int, string, bool) - cấp độ, nickname, online
var tuple = (42, "Cuc_Cool_Player_2024", true);
Thấy chưa? Không cần class, không cần thủ tục rườm rà! Chỉ cần ngoặc tròn, ngăn cách bằng dấu phẩy — xong luôn. Kiểu như fastfood trong thế giới lập trình: nhanh, tiện, đúng cái cần lúc đó.
Điều tuyệt nhất là tuple tạo "tại chỗ", ngay trong code. Quá hợp cho mấy lúc cần truyền nhanh giá trị từ function hoặc gom tạm dữ liệu. Việc gì phải phức tạp với class khi có thể giải quyết gọn nhẹ, đẹp mắt?
3. Cú pháp và khai báo tuple cơ bản
Bắt đầu với kiểu đơn giản nhất: không đặt tên cho phần tử.
var point = (10, 20);
Console.WriteLine(point.Item1); // 10
Console.WriteLine(point.Item2); // 20
Ở đây point là tuple hai phần tử (int, int). Truy cập phần tử qua field Item1, Item2 v.v (tới Item7). Nếu tuple nhiều hơn, phần tử thứ tám trở đi nằm trong field đặc biệt Rest (chút nữa nói sau). point.Item1 và point.Item2 là tên field của struct ValueTuple<int, int>, do compiler tự tạo.
Tại sao dùng var?
(int, int) point = (10, 20);
Mẹo: Dùng var rất tiện vì C# tự suy ra kiểu đúng. Nhưng nếu muốn nhấn mạnh là đang dùng tuple, bạn có thể chỉ rõ kiểu.
4. Đặt tên cho phần tử tuple
Công nhận chứ, Item1 với Item2 nghe chán thật. Làm cho xịn hơn được mà! Đó là lý do có phần tử đặt tên:
var person = (Name: "Alysa", Age: 20);
Console.WriteLine(person.Name); // Alysa
Console.WriteLine(person.Age); // 20
Giờ mỗi phần tử có tên ý nghĩa. Quá đã! Vừa dễ hiểu code, vừa auto-complete trong IDE cũng tiện.
Có thể trộn tên và không tên không?
Có, C# cho phép, nhưng tốt nhất nên đặt tên hết nếu tuple có hơn hai phần tử hoặc giá trị mang ý nghĩa khác nhau:
var data = (42, Name: "Bob", true);
Console.WriteLine(data.Item1); // 42
Console.WriteLine(data.Name); // Bob
Console.WriteLine(data.Item3); // true
5. Trả về và nhận tuple từ method
Tình huống phổ biến nhất: trả về từ method không chỉ một mà nhiều giá trị. Ví dụ, khi phân tích chuỗi, muốn lấy luôn số và trạng thái thành công.
Ví dụ: Tính tổng và tích của hai số
Giả sử bạn cần viết function nhận hai số và trả về cùng lúc tổng và tích của chúng. Nếu không có tuple, bạn phải dùng out-parameter (cái này học sau), hoặc tạo class/struct riêng chỉ cho mục đích này (mà như vậy thì đi ngược lại ý tưởng Value Tuple là "gộp tạm nhiều giá trị khác kiểu, khỏi lăn tăn kiểu dữ liệu riêng").
Trả về nhiều giá trị từ method bằng tuple:
// Method trả về tuple hai giá trị: tổng và tích
(int sum, int product) CalculateSumAndProduct(int a, int b)
{
return (a + b, a * b);
}
// Gọi method
var calculationResult = CalculateSumAndProduct(10, 5);
// In kết quả
Console.WriteLine($"Tổng: {calculationResult.sum}");
Console.WriteLine($"Tích: {calculationResult.product}");
6. Giới hạn và đặc điểm của tuple
Cái gì nhìn hoàn hảo cũng có điểm cần chú ý. Cùng xem nhé.
Số lượng phần tử
Tuple trong C# hỗ trợ tối đa 7 phần tử trực tiếp. Nếu cần nhiều hơn — phần tử thứ tám trở đi sẽ nằm trong tuple lồng bên trong (Rest). Nhưng nếu phải làm vậy, có lẽ bạn nên nghĩ đến struct hoặc class riêng.
var bigTuple = (1, 2, 3, 4, 5, 6, 7, 8);
// .NET tự tạo tuple lồng ở phần tử thứ tám
Console.WriteLine(bigTuple.Item8); // 8
Có thể thay đổi
ValueTuple là struct có thể thay đổi! Bạn có thể sửa bất kỳ field nào sau khi tạo (nhưng đừng lạm dụng, kẻo code thành "ảo thuật" khó hiểu).
var t = (a: 1, b: 2);
t.a = 5; // Được luôn!
7. Tại sao tuple hữu ích trong dự án thực tế và phỏng vấn
Tuple giúp trả về nhiều giá trị từ method nhanh gọn, khỏi tạo thêm class hay struct. Rất tiện khi cần gom dữ liệu tạm thời — ví dụ khi select/filter collection, viết test hoặc mấy phép tính tạm.
Ngoài ra, tuple cực hợp để deconstruct kết quả ra nhiều biến cùng lúc (ví dụ sau LINQ query), làm code dễ đọc, gọn gàng.
Khi phỏng vấn vị trí junior developer, hay bị hỏi: "Làm sao trả về nhiều giá trị từ method?" — và câu trả lời xịn nhất là dùng ValueTuple. Nhớ là đây là struct hỗ trợ đặt tên phần tử và deconstruct, khác với Tuple<T1, T2> kiểu cũ (kiểu reference).
Nhưng nếu bạn làm việc với dữ liệu lưu lâu dài, liên quan business logic, hoặc số field trả về quá nhiều (hơn bảy), tốt nhất vẫn nên tạo class hoặc struct riêng với property có ý nghĩa.
8. Lỗi thường gặp và bẫy khi dùng tuple
Lỗi #1: Gõ sai tên phần tử tuple.
Field Item1, Item2 và phần tử đặt tên nằm trong chính kiểu ValueTuple. Nếu bạn lỡ gõ sai tên, code vẫn compile, nhưng khi chạy hoặc trong IDE sẽ báo không tìm thấy property đó. Luôn kiểm tra tên cho chuẩn nhé!
Lỗi #2: Deconstruct không giữ tên gốc.
Khi bạn unpack tuple không tên bằng var (first, second) = tuple;, biến mới first và second chỉ là biến local, còn Item1/Item2 gốc vẫn không tên. Để tránh rối, nên khai báo tuple với tên rõ ràng luôn:
var tuple = (X: 5, Y: 10);
var (x, y) = tuple; // x == 5, y == 10
Lỗi #3: Không để ý tuple là struct.
ValueTuple là struct, nên khi truyền vào method hoặc gán, nó bị copy. Mọi thay đổi trong function chỉ ảnh hưởng bản copy local, không phải tuple gốc. Nếu bạn mong phần tử tuple thay đổi "tại chỗ", hãy chắc chắn dùng giá trị trả về hoặc kiểu reference.
GO TO FULL VERSION