1. Đặt tên tuple: bối cảnh cho giải cấu trúc
Ở bài trước tụi mình đã làm quen với tuple có phần tử đặt tên, giúp code dễ đọc hơn nhiều, thay vì phải truy cập giá trị qua mấy cái tên vô hồn như Item1, Item2, mà dùng tên có ý nghĩa (ví dụ person.Name). Việc đặt tên cho phần tử tuple đặc biệt quan trọng khi dùng tuple làm giá trị trả về từ method, parameter hoặc property. Chính việc đặt tên này là tiền đề quan trọng cho việc giải cấu trúc tiện lợi mà tụi mình sẽ học tiếp trong bài này.
Nhắc lại nè, bạn có thể đặt tên khi khởi tạo tuple bằng literal, hoặc trong signature của return value của method, property hay field.
Ví dụ với method: Tuple có phần tử đặt tên trong return value của method
public static (int Tuoi, string Ten) GetPetInfo()
{
return (Tuoi: 5, Ten: "Barsik");
}
// Sử dụng:
var info = GetPetInfo();
Console.WriteLine($"{info.Ten} — {info.Tuoi} tuổi");
Ví dụ với field/property: Tuple có phần tử đặt tên trong field/property
public (int Rong, int Cao) KichThuocAnh = (1024, 768);
Nhớ nhé: nếu không đặt tên rõ ràng, các phần tử vẫn sẽ truy cập được qua Item1, Item2 v.v. Điều này làm code khó đọc hơn, nhất là khi dùng tuple nhiều lần. Vì vậy, luôn nên đặt tên có ý nghĩa cho phần tử, trừ khi ý nghĩa đã quá rõ ràng từ context rồi.
2. Giải cấu trúc tuple
Giải cấu trúc là gì?
Giải cấu trúc là quá trình "bóc tách" tuple thành các biến riêng biệt để dễ thao tác. Tức là, từ tuple (Tuoi: 5, Ten: "Barsik") bạn lấy ra hai biến: tuoi và ten.
Ví dụ dễ hiểu: Hãy tưởng tượng tuple là cái hộp có các ngăn được dán nhãn. Giải cấu trúc là bạn lấy từng món trong hộp ra để đúng chỗ trên bàn luôn.
Cú pháp giải cấu trúc
var pet = (Tuoi: 5, Ten: "Barsik");
var (tuoi, ten) = pet;
Console.WriteLine($"{ten} — {tuoi} tuổi");
Giờ bạn có hai biến: tuoi và ten. Chúng nhận giá trị từ tuple. Tên bên trái (tuoi, ten) không nhất thiết phải giống tên trong tuple đâu, chỉ là biến local mới thôi.
Giải cấu trúc return value của function
Tuple thường dùng để trả về nhiều giá trị từ function. Lúc này giải cấu trúc cực kỳ tiện:
public static (double nhoNhat, double lonNhat) GetMinMax(int[] duLieu)
{
int nhoNhat = duLieu.Min();
int lonNhat = duLieu.Max();
return (nhoNhat, lonNhat);
}
var so = new[] { 1, 2, 3, 4, 5 };
var (giaTriNhoNhat, giaTriLonNhat) = GetMinMax(so);
Console.WriteLine($"Nhỏ nhất: {giaTriNhoNhat}, lớn nhất: {giaTriLonNhat}");
Lưu ý là khi giải cấu trúc, bạn đặt tên biến sao cho tiện với context hiện tại là được.
Giải cấu trúc với var
var (a, b) = (10, 20); // int a = 10, b = 20
Giải cấu trúc và discard _
Đôi khi bạn không cần tất cả phần tử trong tuple. Có thể bỏ qua bằng _ (discard). Discard (_) trong tuple là cách nói "biết là có phần tử này nhưng không cần, khỏi tạo biến cho nó".
Tức là bạn giải cấu trúc tuple nhưng để lại "lỗ trống" ở chỗ không cần giá trị. Ngắn gọn, tiện lợi và không rườm rà!
var pet = (Tuoi: 5, Ten: "Barsik", VuiVe: true);
var (tuoi, _, vuiVe) = pet; // chỉ lấy tuoi và vuiVe, bỏ qua ten
Một điều thú vị: discard dùng nhiều để không làm bẩn namespace với mấy biến không cần thiết.
Giải cấu trúc trong vòng lặp foreach
Trong C# bạn có thể duyệt mảng tuple và giải cấu trúc ngay trong foreach luôn:
var pets = new (string Ten, int Tuoi)[]
{
("Barsik", 5),
("Musya", 3),
("Johnny", 7)
};
foreach (var (ten, tuoi) in pets)
{
Console.WriteLine($"{ten} — {tuoi} tuổi");
}
3. Cách hoạt động của tên phần tử và kiểu dữ liệu
Hành vi của phần tử đặt tên
Tên phần tử tuple chỉ là "đường mật cú pháp" thôi, tức là để dễ cho người đọc. Khi compile, tên sẽ thành field nội bộ Item1, Item2 v.v., nhưng IDE và compiler vẫn giữ tên để bạn dùng cho tiện.
Ảnh hưởng đến compatibility kiểu và ép kiểu
Hai tuple có cùng số phần tử và kiểu, nhưng tên khác nhau, vẫn được coi là cùng kiểu với compiler. Tên phần tử không ảnh hưởng đến định nghĩa kiểu:
var t1 = (X: 42, Y: 13);
var t2 = (A: 42, B: 13);
t1 = t2; // OK
Console.WriteLine(t1.X); // 42
Tuy nhiên khi làm việc với expression và gợi ý IntelliSense thì tên bên trái (nơi gán) sẽ được dùng, không phải tên bên phải.
Chỉ định tên rõ ràng và không rõ ràng
Đã học rồi, nhưng nhắc lại là bạn có thể tạo tuple với một phần tử đặt tên, phần còn lại không, hoặc không đặt tên gì hết — lúc đó sẽ là Item1 v.v.
var diem = (X: 10, 20); // X và Item2
Console.WriteLine(diem.X); // 10
Console.WriteLine(diem.Item2); // 20
Tip: luôn đặt tên phần tử nếu tuple có hơn một-hai phần tử, hoặc nếu ý nghĩa giá trị không rõ ràng từ context.
4. Lỗi phổ biến và lưu ý khi làm việc với tên và giải cấu trúc
Lỗi #1: “chuyển nhà” tên khi gán tuple khác nhau
Nếu bạn khai báo tuple với tên này, rồi gán cho nó tuple khác không có tên (hoặc tên khác), thì trong IntelliSense vẫn hiện tên ban đầu. Ví dụ:
var goc = (X: 1, Y: 2);
var bietDanh = goc; // bietDanh.X == 1, bietDanh.Y == 2
goc = (10, 20); // tuple không tên
Console.WriteLine(bietDanh.X); // vẫn chạy, bietDanh vẫn giữ tên cũ
Lỗi #2: trùng tên phần tử.
Không được đặt hai phần tử cùng tên — compiler sẽ báo lỗi “Duplicate tuple element name”:
var tupleLoi = (A: 1, A: 2); // Lỗi CS8122: Duplicate tuple element name 'A'
Lỗi #3: giải cấu trúc sai số lượng phần tử.
Khi giải cấu trúc, số biến phải đúng bằng số phần tử tuple. Sai là lỗi liền:
var pet = (Tuoi: 5, Ten: "Barsik");
var (tuoi, ten, tamTrang) = pet; // Lỗi CS8124: Tuple must contain exactly 3 elements
Lỗi #4: dùng discard _ sai cách
Bỏ qua phần tử bằng _ chỉ áp dụng cho từng vị trí riêng biệt, không “gộp” các chỗ bỏ qua thành một. Nếu bạn cố bỏ qua hai phần tử bằng một _, sẽ bị lỗi:
var duLieu = (1, 2, 3);
var (_, x, _) = duLieu; // Đúng: bỏ qua phần tử 1 và 3
var (_, _) = duLieu; // Lỗi CS8124: Tuple must contain exactly 2 elements
GO TO FULL VERSION