TL;DR: Cách sửa nhanh trong 10 giây
Nếu bạn chỉ cần xem nhanh dữ liệu để gỡ lỗi (debugging), đừng quá lo lắng. Hầu hết các lập trình viên giải quyết vấn đề này qua hai bước nhanh chóng:
- Thêm
#[derive(Debug)]ngay phía trên struct hoặc enum của bạn. - Cập nhật câu lệnh in để sử dụng trình định dạng debug:
println!("{:?}", my_var);.
Bạn cần kết quả hiển thị đẹp mắt cho người dùng cuối? Thay vào đó, bạn sẽ cần triển khai (implement) trait Display một cách thủ công.
Vấn đề: Tại sao Rust lại báo lỗi
Rust cực kỳ chú trọng vào tính tường minh. Không giống như các ngôn ngữ như JavaScript hay Python, Rust sẽ không tự đoán cách bạn muốn chuyển đổi một cấu trúc dữ liệu phức tạp thành một chuỗi ký tự (string). Trong khi các kiểu dữ liệu phổ biến như i32 hoặc String đã có sẵn định dạng, các kiểu dữ liệu tùy chỉnh của bạn vẫn là một tờ giấy trắng.
Có lẽ bạn đã gặp phải rào cản này khi cố gắng chạy đoạn mã như sau:
struct User {
id: u32,
username: String,
}
fn main() {
let user = User { id: 1, username: String::from("alice") };
// Dòng này gây ra lỗi E0277
println!("{}", user);
}
Khi bạn sử dụng {}, bạn đang yêu cầu Rust sử dụng trait Display. Vì bạn chưa định nghĩa User sẽ hiển thị như thế nào, trình biên dịch (compiler) sẽ chặn bạn ngay lập tức với một thông báo như sau:
error[E0277]: `User` doesn't implement `std::fmt::Display`
--> src/main.rs:10:20
|
10 | println!("{}", user);
| ^^^^ `User` cannot be formatted with the default formatter
Giải pháp 1: Trait Debug (Tốt nhất cho quá trình phát triển)
Trait Debug là cách tiêu chuẩn để kiểm tra các biến trong quá trình phát triển. Nó hiển thị tên struct và tất cả các trường (field) bên trong mà bạn không cần phải viết bất kỳ dòng logic định dạng nào.
Cách áp dụng:
#[derive(Debug)] // 1. Chỉ cần thêm attribute này
struct User {
id: u32,
username: String,
}
fn main() {
let user = User { id: 1, username: String::from("alice") };
// 2. Sử dụng {:?} để in trên một dòng, hoặc {:#?} để hiển thị "đẹp" trên nhiều dòng
println!("{:?}", user);
}
Kết quả: User { id: 1, username: "alice" }
Đối với các struct lớn có hơn 20 trường, hãy luôn sử dụng {:#?}. Nó sẽ thêm các khoảng thụt đầu dòng và xuống dòng, giúp kết quả dễ đọc hơn nhiều trong một terminal dày đặc thông tin.
Giải pháp 2: Triển khai Display (Tốt nhất cho người dùng)
Nếu bạn đang xây dựng một công cụ dòng lệnh, người dùng của bạn sẽ không muốn thấy User { id: 1, username: "alice" }. Họ chỉ muốn thấy thông tin cần thiết. Vì mỗi ứng dụng có nhu cầu khác nhau, Rust yêu cầu bạn phải tự viết logic này một cách thủ công.
Cách triển khai:
use std::fmt;
struct User {
id: u32,
username: String,
}
impl fmt::Display for User {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Định dạng đầu ra chính xác theo cách bạn muốn
write!(f, "User #{} ({})", self.id, self.username)
}
}
fn main() {
let user = User { id: 1, username: String::from("alice") };
// Bây giờ trình giữ chỗ {} đơn giản đã hoạt động hoàn hảo
println!("{}", user);
}
Kết quả: User #1 (alice)
Tại sao Rust không tự động làm việc này?
An toàn và bảo mật là những lý do cốt lõi. Hãy tưởng tượng một struct chứa password_hash hoặc private_key. Nếu Rust tự động triển khai biểu diễn chuỗi, bạn có thể vô tình làm rò rỉ dữ liệu nhạy cảm vào log hoặc console. Bằng cách buộc bạn phải chọn giữa Debug và Display, Rust đảm bảo rằng bạn có chủ đích về việc dữ liệu nào sẽ được đưa ra khỏi chương trình.
Kiểm tra lại kết quả
Thực hiện các bước sau để đảm bảo lỗi đã được khắc phục hoàn toàn:
- Chạy
cargo check. Nếu lỗi E0277 biến mất, các trait của bạn đã được thỏa mãn. - Chạy
cargo runđể xác nhận kết quả hiển thị. - Nếu bạn sử dụng
Debug, hãy xác nhận rằng tất cả các trường đều hiển thị. - Nếu bạn sử dụng
Display, hãy kiểm tra xem định dạng có khớp với logic trong macrowrite!của bạn không.
Cẩn thận với các kiểu dữ liệu lồng nhau
Các trait trong Rust có tính đệ quy. Nếu struct của bạn chứa một sub-struct tùy chỉnh, sub-struct đó cũng phải triển khai trait tương ứng. Nếu bạn quên, trình biên dịch sẽ báo lỗi ở struct cha ngay cả khi nó đã có attribute derive.
#[derive(Debug)]
struct Metadata {
created_at: u64,
}
#[derive(Debug)] // Hoạt động bình thường vì Metadata cũng derive Debug
struct Task {
name: String,
meta: Metadata,
}
Tài liệu tham khảo hữu ích
- Tài liệu std::fmt chính thức: Hướng dẫn chi tiết về định dạng.
- Rust by Example: Hướng dẫn trực quan cho Display và Debug.
- crate derive_more: Một công cụ tuyệt vời nếu bạn đã mệt mỏi với việc viết triển khai Display thủ công cho hàng tá kiểu dữ liệu.

