Bỏ qua để đến nội dung

Tuple trong Rust

Tuple gom nhiều giá trị, có thể thuộc các kiểu khác nhau, lại thành một. Đây là cách nhanh để gói những thứ liên quan với nhau mà không cần định nghĩa hẳn một kiểu mới.

fn main() {
let person: (&str, i32, bool) = ("Alice", 30, true);
println!("{:?}", person);
}

Kết quả:

("Alice", 30, true)
▶ Tự chạy thử

Để ý rằng tuple này trộn một &str, một i32 và một bool trong cùng một giá trị — điều mà mảng không làm được, vì mọi phần tử trong mảng phải cùng kiểu.

Để in cả một tuple, hãy dùng {:?} (định dạng debug) thay vì {}. {} thông thường không dùng được cho tuple.

Dùng .0, .1, .2, và cứ thế — vị trí trong tuple được đánh số từ 0:

fn main() {
let person = ("Alice", 30, true);
println!("Tên: {}", person.0);
println!("Tuổi: {}", person.1);
println!("Đang hoạt động: {}", person.2);
}

Kết quả:

Tên: Alice
Tuổi: 30
Đang hoạt động: true
▶ Tự chạy thử

Vị trí phải là một con số viết thẳng trong code. Bạn không thể dùng biến, như person.i, vì mỗi vị trí có thể chứa một kiểu khác nhau, và Rust phải biết kiểu ngay lúc biên dịch.

Bạn có thể tách mọi giá trị ra thành từng biến riêng chỉ trong một dòng (gọi là destructuring, “phân rã”):

fn main() {
let person = ("Alice", 30, true);
let (name, age, is_active) = person;
println!("{name} {age} tuổi, đang hoạt động: {is_active}");
}

Kết quả:

Alice 30 tuổi, đang hoạt động: true
▶ Tự chạy thử

Cách này thường dễ đọc hơn việc lặp đi lặp lại .0, .1, .2, nhất là khi tuple có hơn hai ba phần tử.

Tuple là cách tự nhiên để trả về nhiều hơn một giá trị, vì một hàm chỉ khai báo được một kiểu trả về:

fn min_max(numbers: &[i32]) -> (i32, i32) {
let min = *numbers.iter().min().unwrap();
let max = *numbers.iter().max().unwrap();
(min, max)
}
fn main() {
let (smallest, largest) = min_max(&[4, 8, 1, 9, 3]);
println!("nhỏ nhất: {smallest}, lớn nhất: {largest}");
}

Kết quả:

nhỏ nhất: 1, lớn nhất: 9
▶ Tự chạy thử

Đừng bận tâm về &[i32], .iter() hay .unwrap() lúc này. Chúng được giải thích trong các bài Slice, Iterator và Option. Điều cần để ý ở đây là kiểu trả về (i32, i32) và cách main tách nó ngay thành hai biến.

Nếu tuple được khai báo với mut, bạn có thể thay đổi từng phần tử của nó, dùng cùng các vị trí .0, .1. Giá trị thì thay đổi được, nhưng hình dạng thì không: một tuple luôn giữ nguyên số lượng và kiểu của các phần tử như lúc ban đầu.

Destructuring cũng hoạt động theo chiều ngược lại: bạn có thể gán vào các biến đã có. Nhờ đó bạn có cách hoán đổi gọn trong một dòng, không cần biến tạm. Nếu chỉ muốn lấy vài phần tử, hãy viết _ cho những phần tử bạn không cần:

fn main() {
let mut point = (3, 4);
point.0 += 10; // thay đổi một phần tử tại chỗ
println!("point là {:?}", point);
let mut a = 1;
let mut b = 2;
(a, b) = (b, a); // hoán đổi hai giá trị
println!("a = {a}, b = {b}");
let (x, _) = point; // giữ x, bỏ qua phần tử thứ hai
println!("x = {x}");
}

Kết quả:

point là (13, 4)
a = 2, b = 1
x = 13
▶ Tự chạy thử

Phép hoán đổi chạy được vì Rust dựng vế phải, (b, a), trước. Chỉ sau đó nó mới tách ra vào a và b. Bạn còn làm được nhiều điều hơn nữa với _ và mẫu trong bài So khớp mẫu.

Thật ra bạn đã gặp một tuple mà không nhận ra. () được gọi là kiểu unit, và đó là thứ một hàm trả về khi nó không trả về gì có ý nghĩa — nó là kiểu trả về ngầm định của fn main().

Tuple rất hợp cho những nhóm nhanh, không cần tên — đặc biệt là trả về hai ba giá trị từ một hàm. Khi một nhóm giá trị biểu diễn một khái niệm thật sự với các field có tên (như một Person có tên và tuổi mà bạn sẽ gọi bằng tên khắp nơi trong code), một struct thường là lựa chọn tốt hơn và dễ đọc hơn.

Viết hàm sum_and_product(a: i32, b: i32) -> (i32, i32) trả về cả tổng lẫn tích của hai số. Trong main, gọi nó với 5 và 6 rồi destructuring kết quả. Gợi ý: dòng cuối của hàm chính là tuple, (a + b, a * b), không có dấu chấm phẩy.

Xem lời giải
fn sum_and_product(a: i32, b: i32) -> (i32, i32) {
(a + b, a * b)
}
fn main() {
let (sum, product) = sum_and_product(5, 6);
println!("tổng = {sum}, tích = {product}");
}

Kết quả:

tổng = 11, tích = 30
▶ Tự chạy thử

Một hình chữ nhật có thể lưu dưới dạng tuple gồm hai điểm, trong đó mỗi điểm lại là một tuple: let rect = ((1, 2), (5, 5));. Hãy tính chiều rộng, chiều cao và diện tích của nó. Gợi ý: tuple có thể lồng nhau, và rect.1.0 nghĩa là “phần tử đầu tiên của điểm thứ hai”.

Xem lời giải
fn main() {
let rect = ((1, 2), (5, 5));
let width = rect.1.0 - rect.0.0;
let height = rect.1.1 - rect.0.1;
println!("rộng {width}, cao {height}, diện tích {}", width * height);
}

Kết quả:

rộng 4, cao 3, diện tích 12
▶ Tự chạy thử

Bạn cũng có thể destructuring cả hai điểm cùng lúc: let ((x1, y1), (x2, y2)) = rect;. Gõ dài hơn, nhưng x2 - x1 dễ đọc hơn rect.1.0 - rect.0.0. Khi các dấu chấm bắt đầu chồng chất như thế này, đó là dấu hiệu một struct có field đặt tên sẽ rõ ràng hơn.

  • Tuple gom các giá trị có thể khác kiểu: (name, age, is_active).
  • Truy cập phần tử bằng .0, .1, .2, hoặc tách tất cả ra cùng lúc bằng destructuring.
  • Tuple là cách tự nhiên để trả về nhiều giá trị từ một hàm.
  • Phần tử của tuple mut thay đổi được, nhưng kích thước và kiểu thì không. (a, b) = (b, a); hoán đổi hai giá trị.
  • Với những nhóm có tên và dùng lại nhiều lần, hãy dùng struct.

Kiểm tra nhanh

1. Tuple chứa được gì mà mảng không chứa được?

2. Làm sao truy cập phần tử thứ hai của tuple tên person?

3. Sau let mut a = 1; let mut b = 2; (a, b) = (b, a);, a và b là bao nhiêu?

Điểm: 0 / 3