Chuyển tới nội dung chính

Tuple

Định nghĩa

Một tuple là cấu trúc dữ liệu nhóm nhiều giá trị thành một đơn vị duy nhất mà không cần định nghĩa một kiểu chuyên biệt. C# hỗ trợ hai hệ thống tuple: System.Tuple cũ hơn (kiểu tham chiếu, C# 4) và System.ValueTuple hiện đại (kiểu giá trị, C# 7+). ValueTuple là cách tiếp cận được khuyến nghị với hỗ trợ ngôn ngữ cấp một cho các phần tử có tên, phân rã (Deconstruction), và khớp mẫu (Pattern Matching).

// Lightweight grouping without a custom type
(int Id, string Name, double Score) result = (1, "Alice", 95.5);
Console.WriteLine($"{result.Name}: {result.Score}"); // Alice: 95.5

Khái niệm cốt lõi

System.Tuple (Tuple tham chiếu)

Được giới thiệu trong .NET Framework 4.0. Các thực thể là kiểu tham chiếu (Reference Type) được cấp phát trên heap. Các phần tử được truy cập dưới dạng Item1, Item2, ..., Item8 — không có hỗ trợ tên phần tử ở cấp ngôn ngữ.

// Creating a Tuple
Tuple<int, string, bool> person = Tuple.Create(1, "Alice", true);

// Accessing elements — no named members
int id = person.Item1;
string name = person.Item2;
bool active = person.Item3;

// Maximum 8 elements; 8th position uses TRest for nesting
var big = Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(8, 9));
System.Tuple là cũ

System.Tuple dài dòng, cấp phát trên heap, và không có phần tử có tên ở cấp ngôn ngữ. Ưu tiên System.ValueTuple cho mọi mã mới.

System.ValueTuple (Tuple giá trị)

Được giới thiệu trong C# 7 (.NET Framework 4.7+ / .NET Core). Kiểu giá trị (Value Type) được cấp phát trên ngăn xếp (Stack) với hỗ trợ ngôn ngữ cấp một cho phần tử có tên, phân rã, và so sánh bằng.

// Unnamed elements
(int, string) person1 = (1, "Alice");
int id = person1.Item1;

// Named elements (recommended)
(int Id, string Name) person2 = (2, "Bob");
Console.WriteLine(person2.Name); // Bob

// Target-typed with var
var point = (X: 3.0, Y: 4.0);
Console.WriteLine($"({point.X}, {point.Y})"); // (3, 4)

// No practical element limit
var big = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Phần tử có tên chỉ tồn tại tại thời điểm biên dịch

Các tên như (int Id, string Name) chỉ tồn tại tại thời điểm biên dịch. Tại thời điểm chạy, chúng trở thành Item1Item2. Reflection chỉ thấy các tên được đánh số.

Phân rã Tuple (Tuple Deconstruction)

Trích xuất các phần tử tuple thành các biến riêng lẻ trong một câu lệnh duy nhất. Hỗ trợ var cho suy luận kiểu và _ để loại bỏ phần tử.

// Basic deconstruction
var (id, name, active) = (1, "Alice", true);

// Discard elements with _
var (x, y, _) = (3.0, 4.0, "unused");

// Deconstruct method return
var (count, average) = GetStats(new[] { 80, 90, 100 });
Console.WriteLine($"Count: {count}, Avg: {average}");

// Deconstruct into existing variables
int a, b;
(a, b) = (10, 20);

Phân rã tùy chỉnh (Custom Deconstruct) — bất kỳ kiểu nào cũng có thể hỗ trợ phân rã bằng cách định nghĩa phương thức Deconstruct:

public class Point
{
public double X { get; init; }
public double Y { get; init; }

public void Deconstruct(out double x, out double y)
{
x = X;
y = Y;
}
}

var p = new Point { X = 3, Y = 4 };
var (x, y) = p; // Calls Deconstruct

Tuple làm kiểu trả về phương thức

Tuple thay thế tham số out để trả về nhiều giá trị từ một phương thức:

// Method returning a named tuple
public (int Count, double Average) GetStats(IEnumerable<int> numbers)
{
var list = numbers.ToList();
return (list.Count, list.Average());
}

// Caller usage
var stats = GetStats(new[] { 80, 90, 100 });
Console.WriteLine($"{stats.Count} items, avg {stats.Average}");

// Or deconstruct directly
var (count, avg) = GetStats(new[] { 80, 90, 100 });
Tuple vs API công khai

Tuple làm kiểu trả về lý tưởng cho phương thức private/internal. Đối với API công khai, ưu tiên class hoặc record có tên rõ ràng để đảm bảo tính rõ ràng, khả năng khám phá và khả năng phiên bản.

Mẫu Tuple trong biểu thức Switch

C# 8+ hỗ trợ khớp mẫu trên tuple trong biểu thức switch — một sự kết hợp mạnh mẽ:

string Classify(int x, int y) => (x, y) switch
{
(0, 0) => "Origin",
(> 0, > 0) => "First quadrant",
(< 0, > 0) => "Second quadrant",
(< 0, < 0) => "Third quadrant",
(> 0, < 0) => "Fourth quadrant",
(0, _) or (_, 0) => "On axis"
};

// Rock-paper-scissors example
string Rps(string p1, string p2) => (p1, p2) switch
{
("rock", "scissors") or ("scissors", "paper") or ("paper", "rock")
=> "Player 1 wins",
("rock", "rock") or ("scissors", "scissors") or ("paper", "paper")
=> "Draw",
_ => "Player 2 wins"
};

Xem Khớp mẫu (Pattern Matching) để biết hướng dẫn đầy đủ về khớp mẫu.

Tuple với LINQ

Tuple cung cấp giải pháp thay thế nhẹ cho kiểu ẩn danh (Anonymous Types) trong phép chiếu LINQ:

// Tuple projection instead of anonymous type
var topStudents = students
.Select(s => (s.Name, s.Grade))
.Where(t => t.Grade >= 90)
.ToList();

// GroupBy with tuple result
var grouped = orders
.GroupBy(o => (o.Category, o.Year))
.Select(g => (g.Key, Total: g.Sum(o => o.Amount)));

// Zip two sequences into tuples
var pairs = names.Zip(scores)
.Select(z => (z.First, z.Second));

// Aggregate with tuple accumulator
var (min, max) = numbers.Aggregate(
(Min: int.MaxValue, Max: int.MinValue),
(acc, n) => (Math.Min(acc.Min, n), Math.Max(acc.Max, n)));

So sánh bằng Tuple (Tuple Equality)

C# 7.3+ hỗ trợ ==!= trên tuple. So sánh được thực hiện từng phần tử sử dụng phép so sánh bằng mặc định của từng kiểu phần tử:

(1, "a") == (1, "a") // true
(1, "a") != (1, "b") // true
(1, 2, 3) == (1, 2, 3) // true

// Works with named elements too — names don't affect equality
(int X, int Y) p1 = (1, 2);
(int A, int B) p2 = (1, 2);
p1 == p2 // true — structural equality

So sánh Tuple vs ValueTuple

Tính năngSystem.TupleSystem.ValueTuple
KiểuKiểu tham chiếu (class)Kiểu giá trị (struct)
Cấp phátHeapStack
Phần tử có tênKhông (chỉ Item1, Item2, ...)Có, ở cấp ngôn ngữ
Giới hạn phần tử8 (cần lồng nhau cho nhiều hơn)Không có giới hạn thực tế
So sánh bằng (==)So sánh tham chiếuSo sánh giá trị (C# 7.3+)
Phân rãKhông hỗ trợĐược hỗ trợ
Có thể thay đổiKhông (chỉ đọc)Có (các trường có thể thay đổi)
Phiên bản C#C# 4C# 7+
Được khuyến nghịKhông (cũ)Có (lựa chọn mặc định)

Khi nào sử dụng

Tình huốngKhuyến nghị
Nhiều giá trị trả về từ phương thức private/internalSử dụng ValueTuple
Nhóm dữ liệu tạm thời trong truy vấn LINQSử dụng ValueTuple
Biểu thức switch với nhiều đầu vàoSử dụng ValueTuple
Khóa tổ hợp (Composite Key) cho DictionaryValueTuple phù hợp
Kiểu trả về API công khaiSử dụng record hoặc class có tên
Nhiều hơn 4-5 phần tửCân nhắc record hoặc class
Dữ liệu cần tuần tự hóaTránh tuple; sử dụng record/class

Lỗi thường gặp

  1. Tên phần tử bị xóa tại thời điểm chạy — Các phần tử có tên như (int Id, string Name) chỉ tồn tại tại thời điểm biên dịch. Reflection chỉ thấy Item1, Item2. Chúng không tồn tại qua các ép kiểu object trong mọi tình huống.

  2. ValueTuple có thể thay đổi — Khác với Tuple, các trường ValueTuple có thể thay đổi. Sử dụng ValueTuple làm khóa dictionary và sau đó thay đổi nó sẽ làm hỏng dictionary vì mã băm thay đổi.

  3. Nhầm lẫn System.Tuple với System.ValueTuple — Viết new Tuple<int, string>(1, "a") khi ý là (1, "a"). Cái trước cấp phát trên heap và thiếu các tính năng ngôn ngữ.

  4. Lạm dụng tuple cho dữ liệu phức tạp — Khi tuple phát triển vượt quá 3-4 phần tử, record cung cấp tài liệu tốt hơn, hỗ trợ tuần tự hóa và khớp mẫu. Tuple dành cho nhóm dữ liệu nhẹ và tạm thời.

Điểm chính cần nhớ

  1. Luôn sử dụng System.ValueTuple ((T1, T2)) thay vì System.Tuple cho mã mới.
  2. Phần tử có tên cải thiện khả năng đọc: (int Id, string Name) tốt hơn (int, string).
  3. Phân rã tuple (Deconstruction) và biểu thức switch là sự kết hợp mạnh mẽ.
  4. Tuple phù hợp nhất cho mã nội bộ; ưu tiên record cho API công khai.
  5. ValueTuple là kiểu giá trị có thể thay đổi — không sử dụng làm khóa dictionary nếu bạn có ý định thay đổi nó.

Câu hỏi phỏng vấn

H: Sự khác biệt giữa TupleValueTuple là gì?

Tuple (C# 4) là kiểu tham chiếu trên heap với các phần tử được truy cập dưới dạng Item1, Item2, v.v. ValueTuple (C# 7) là kiểu giá trị trên ngăn xếp (Stack) với phần tử có tên ở cấp ngôn ngữ, phân rã, so sánh bằng giá trị, và không có giới hạn phần tử thực tế. Luôn ưu tiên ValueTuple cho mã mới.

H: Bạn có thể sử dụng tuple làm khóa dictionary không?

Có, ValueTuple triển khai GetHashCodeEquals dựa trên các phần tử của nó, nên (int, string) hoạt động làm khóa dictionary. Tuy nhiên, vì ValueTuple có thể thay đổi, bạn không được thay đổi tuple sau khi sử dụng nó làm khóa.

H: Phân rã tuple (Tuple Deconstruction) là gì?

Phân rã trích xuất các phần tử tuple thành các biến riêng biệt trong một câu lệnh duy nhất: var (name, age) = GetPerson();. Bất kỳ kiểu nào có phương thức Deconstruct (hoặc phương thức mở rộng) đều hỗ trợ phân rã, không chỉ tuple.

H: Tên phần tử tuple được xử lý như thế nào tại thời điểm chạy?

Tên phần tử tuple như (int Id, string Name) chỉ tồn tại tại thời điểm biên dịch. Tại thời điểm chạy, các tên trở thành Item1Item2. Chúng được bảo toàn trong các thuộc tính cho reflection trong một số tình huống nhưng không thuộc về kiểu tại thời điểm thực thi.

H: Khi nào nên sử dụng tuple thay vì record?

Sử dụng tuple cho nhóm dữ liệu nhẹ, tạm thời trong mã nội bộ (giá trị trả về phương thức, phép chiếu LINQ). Sử dụng record cho API công khai, dữ liệu được tuần tự hóa, hoặc khi bạn cần so sánh cấu trúc (Structural Equality), kế thừa, hoặc nhiều hơn 3-4 trường có tên.

Tài liệu tham khảo