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

Struct trong Rust

Struct gom các giá trị liên quan thành một kiểu có tên. Nếu tuple là một gói nhanh, không tên, thì struct đặt tên cho từng field — khiến code dùng nó dễ đọc hơn rất nhiều.

struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person {
name: String::from("Alice"),
age: 30,
};
println!("{} {} tuổi", alice.name, alice.age);
}

Kết quả:

Alice 30 tuổi
▶ Tự chạy thử

So với tuple ("Alice", 30) — với struct, alice.name và alice.age tự giải thích ý nghĩa. Bạn không phải nhớ rằng phần tử 0 là tên và phần tử 1 là tuổi.

Giống như biến, các instance (thể hiện) của struct mặc định là bất biến. Thêm mut để thay đổi một field sau khi tạo:

struct Person {
name: String,
age: u32,
}
fn main() {
let mut alice = Person {
name: String::from("Alice"),
age: 30,
};
alice.age += 1;
println!("{} giờ đã {} tuổi", alice.name, alice.age);
}
▶ Tự chạy thử

Bạn có thể gắn hàm vào struct bằng một khối impl. Bên trong, &self chỉ chính instance mà phương thức được gọi trên đó:

struct Person {
name: String,
age: u32,
}
impl Person {
fn greet(&self) {
println!("Chào, mình là {} và mình {} tuổi", self.name, self.age);
}
}
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
alice.greet();
}

Kết quả:

Chào, mình là Alice và mình 30 tuổi
▶ Tự chạy thử

Một mẫu phổ biến là hàm new bên trong impl để tạo instance giúp bạn, nhờ đó nơi gọi không phải tự viết ra từng field:

struct Person {
name: String,
age: u32,
}
impl Person {
fn new(name: &str, age: u32) -> Person {
Person { name: name.to_string(), age }
}
}
fn main() {
let bob = Person::new("Bob", 25);
println!("{} {} tuổi", bob.name, bob.age);
}
▶ Tự chạy thử

Để ý age đứng một mình bên trong Person { name: name.to_string(), age } — khi một biến có tên trùng khớp với tên field, bạn có thể bỏ qua việc viết age: age mà chỉ cần viết age. Rust gọi đây là field init shorthand (cách viết tắt khi khởi tạo field).

Thử println!("{:?}", alice) trên struct như đã viết ở trên, và nó sẽ không biên dịch — Rust không biết cách in debug một kiểu tự định nghĩa trừ khi bạn bảo nó. Thêm #[derive(Debug)] phía trên struct để có tính năng đó miễn phí:

#[derive(Debug)]
struct Person {
name: String,
age: u32,
}
fn main() {
let alice = Person { name: String::from("Alice"), age: 30 };
println!("{:?}", alice);
}

Kết quả:

Person { name: "Alice", age: 30 }
▶ Tự chạy thử

#[derive(...)] là cách Rust tự động sinh ra các chức năng thông dụng cho một kiểu — bạn sẽ thấy Debug, và sau này là Clone cùng nhiều trait khác, được thêm theo cách này liên tục.

Tạo struct Rectangle với các field width và height (u32). Cho nó hai phương thức: area, trả về diện tích, và is_square, trả về true khi hai cạnh bằng nhau. Gợi ý: cả hai phương thức chỉ đọc các field, nên chúng nhận &self.

Xem lời giải
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
fn is_square(&self) -> bool {
self.width == self.height
}
}
fn main() {
let wide = Rectangle { width: 8, height: 3 };
let square = Rectangle { width: 5, height: 5 };
println!("hình dài: diện tích {}, vuông? {}", wide.area(), wide.is_square());
println!("hình vuông: diện tích {}, vuông? {}", square.area(), square.is_square());
}

Kết quả:

hình dài: diện tích 24, vuông? false
hình vuông: diện tích 25, vuông? true
▶ Tự chạy thử

Tạo struct Account với owner (String) và balance tính bằng xu (u32). Viết Account::new("Ana") bắt đầu số dư ở 0. Sau đó nạp 50 và 25 xu bằng cách thay đổi field, rồi in tài khoản theo hai cách: bằng một câu bình thường và bằng {:?}. Gợi ý: biến phải là let mut thì bạn mới thay đổi được account.balance, và {:?} cần #[derive(Debug)].

Xem lời giải
#[derive(Debug)]
struct Account {
owner: String,
balance: u32,
}
impl Account {
fn new(owner: &str) -> Account {
Account { owner: owner.to_string(), balance: 0 }
}
}
fn main() {
let mut account = Account::new("Ana");
account.balance += 50;
account.balance += 25;
println!("{} có {} xu", account.owner, account.balance);
println!("{:?}", account);
}

Kết quả:

Ana có 75 xu
Account { owner: "Ana", balance: 75 }
▶ Tự chạy thử

new tự đặt balance: 0, nên nơi gọi không thể quên. Đó là một phần lớn lý do các hàm constructor phổ biến đến vậy.

Đặt ba giá trị Person vào một vector: Ana (34), Ben (16) và Chen (52). In tên của mọi người từ 18 tuổi trở lên, rồi in ra ai lớn tuổi nhất. Gợi ý: duyệt bằng for person in &people, và theo dõi người lớn tuổi nhất cho đến lúc đó bằng let mut oldest = &people[0];.

Xem lời giải
struct Person {
name: String,
age: u32,
}
fn main() {
let people = vec![
Person { name: String::from("Ana"), age: 34 },
Person { name: String::from("Ben"), age: 16 },
Person { name: String::from("Chen"), age: 52 },
];
let mut oldest = &people[0];
for person in &people {
if person.age >= 18 {
println!("{} là người trưởng thành", person.name);
}
if person.age > oldest.age {
oldest = person;
}
}
println!("Lớn tuổi nhất: {} ({})", oldest.name, oldest.age);
}

Kết quả:

Ana là người trưởng thành
Chen là người trưởng thành
Lớn tuổi nhất: Chen (52)
▶ Tự chạy thử

oldest là một tham chiếu (&Person) trỏ vào vector, không phải bản sao, nên không có gì bị clone. Mỗi khi tìm thấy người lớn tuổi hơn, ta chỉ việc cho oldest trỏ sang người đó.

  • Struct gom các field có tên thành một kiểu: struct Person { name: String, age: u32 }.
  • Tạo instance bằng Person { name: ..., age: ... }; đánh dấu mut để thay đổi field về sau.
  • Thêm hành vi bằng khối impl; &self chỉ instance mà phương thức được gọi trên đó.
  • Thêm #[derive(Debug)] để in được struct bằng {:?}.

Kiểm tra nhanh

1. Ưu điểm chính của struct so với tuple là gì?

2. Thêm #[derive(Debug)] vào struct cho phép bạn làm gì?

3. Trong Person { name: name.to_string(), age }, chữ age đứng một mình có tác dụng gì?

Điểm: 0 / 3