Chương trình Ví dụ Sử dụng Structs
Để hiểu khi nào chúng ta nên sử dụng structs, hãy viết một chương trình tính diện tích của hình chữ nhật. Chúng ta sẽ bắt đầu bằng việc sử dụng các biến đơn lẻ, và sau đó cải tiến chương trình cho đến khi chúng ta sử dụng structs.
Hãy tạo một dự án binary mới với Cargo có tên rectangles để tính diện tích hình chữ nhật dựa trên chiều rộng và chiều cao được chỉ định bằng pixel. Listing 5-8 hiển thị một chương trình ngắn với một cách thực hiện chính xác điều đó trong tệp src/main.rs của dự án chúng ta.
fn main() { let width1 = 30; let height1 = 50; println!( "The area of the rectangle is {} square pixels.", area(width1, height1) ); } fn area(width: u32, height: u32) -> u32 { width * height }
Bây giờ, chạy chương trình này bằng cargo run
:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.42s
Running `target/debug/rectangles`
The area of the rectangle is 1500 square pixels.
Đoạn mã này thành công trong việc tính diện tích của hình chữ nhật bằng cách gọi
hàm area
với mỗi kích thước, nhưng chúng ta có thể làm nhiều hơn nữa để làm
cho mã này rõ ràng và dễ đọc hơn.
Vấn đề với đoạn mã này thể hiện rõ trong chữ ký của hàm area
:
fn main() {
let width1 = 30;
let height1 = 50;
println!(
"The area of the rectangle is {} square pixels.",
area(width1, height1)
);
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
Hàm area
được cho là để tính diện tích của một hình chữ nhật, nhưng hàm mà
chúng ta đã viết có hai tham số, và không có chỗ nào trong chương trình của
chúng ta làm rõ rằng các tham số này có liên quan đến nhau. Sẽ dễ đọc và dễ quản
lý hơn nếu nhóm chiều rộng và chiều cao lại với nhau. Chúng ta đã thảo luận về
một cách chúng ta có thể làm điều đó trong phần "Kiểu
Tuple" của Chương 3: bằng cách sử dụng các
tuple.
Cải tiến với Tuples
Listing 5-9 hiển thị một phiên bản khác của chương trình chúng ta sử dụng tuples.
fn main() { let rect1 = (30, 50); println!( "The area of the rectangle is {} square pixels.", area(rect1) ); } fn area(dimensions: (u32, u32)) -> u32 { dimensions.0 * dimensions.1 }
Ở một khía cạnh, chương trình này tốt hơn. Tuples cho phép chúng ta thêm một chút cấu trúc và giờ đây chúng ta chỉ truyền một đối số. Nhưng ở một khía cạnh khác, phiên bản này ít rõ ràng hơn: tuples không đặt tên cho các phần tử của chúng, vì vậy chúng ta phải lập chỉ mục vào các phần của tuple, làm cho phép tính của chúng ta kém rõ ràng hơn.
Việc nhầm lẫn chiều rộng và chiều cao sẽ không ảnh hưởng đến phép tính diện
tích, nhưng nếu chúng ta muốn vẽ hình chữ nhật trên màn hình, điều đó sẽ quan
trọng! Chúng ta sẽ phải nhớ rằng width
là chỉ mục tuple 0
và height
là chỉ
mục tuple 1
. Điều này sẽ càng khó hơn cho người khác để hiểu và ghi nhớ nếu họ
sử dụng mã của chúng ta. Bởi vì chúng ta chưa truyền đạt ý nghĩa của dữ liệu
trong mã của mình, việc dễ gây ra lỗi hơn.
Cải tiến với Structs: Thêm Ý nghĩa
Chúng ta sử dụng structs để thêm ý nghĩa bằng cách gắn nhãn cho dữ liệu. Chúng ta có thể chuyển đổi tuple chúng ta đang sử dụng thành một struct với một tên cho toàn bộ cũng như các tên cho các phần, như được hiển thị trong Listing 5-10.
struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", area(&rect1) ); } fn area(rectangle: &Rectangle) -> u32 { rectangle.width * rectangle.height }
Ở đây, chúng ta đã định nghĩa một struct và đặt tên nó là Rectangle
. Bên trong
dấu ngoặc nhọn, chúng ta định nghĩa các trường là width
và height
, cả hai
đều có kiểu u32
. Sau đó, trong main
, chúng ta tạo một instance cụ thể của
Rectangle
có chiều rộng 30
và chiều cao 50
.
Hàm area
của chúng ta bây giờ được định nghĩa với một tham số, mà chúng ta đã
đặt tên là rectangle
, có kiểu là một tham chiếu không thể thay đổi đến một
instance struct Rectangle
. Như đã đề cập trong Chương 4, chúng ta muốn mượn
struct thay vì lấy quyền sở hữu của nó. Bằng cách này, main
giữ quyền sở hữu
và có thể tiếp tục sử dụng rect1
, đó là lý do tại sao chúng ta sử dụng &
trong chữ ký hàm và nơi chúng ta gọi hàm.
Hàm area
truy cập vào các trường width
và height
của instance Rectangle
(lưu ý rằng việc truy cập các trường của một instance struct đã được mượn không
di chuyển các giá trị trường, đó là lý do tại sao bạn thường thấy việc mượn các
struct). Chữ ký hàm cho area
bây giờ nói chính xác những gì chúng ta muốn:
tính diện tích của Rectangle
, sử dụng các trường width
và height
. Điều này
truyền đạt rằng chiều rộng và chiều cao có liên quan đến nhau, và nó đưa ra các
tên mô tả cho các giá trị thay vì sử dụng các giá trị chỉ mục tuple 0
và 1
.
Đây là một thắng lợi cho sự rõ ràng.
Thêm Chức năng Hữu ích với Derived Traits
Sẽ rất hữu ích nếu có thể in ra một instance của Rectangle
trong khi chúng ta
đang gỡ lỗi chương trình và xem các giá trị cho tất cả các trường của nó.
Listing 5-11 thử sử dụng macro println!
như chúng ta
đã sử dụng trong các chương trước. Tuy nhiên, điều này sẽ không hoạt động.
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
println!("rect1 is {rect1}");
}
Khi chúng ta biên dịch đoạn mã này, chúng ta nhận được một lỗi với thông báo cốt lõi này:
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`
Macro println!
có thể thực hiện nhiều loại định dạng, và theo mặc định, dấu
ngoặc nhọn báo cho println!
sử dụng định dạng được gọi là Display
: đầu ra
dành cho người dùng cuối trực tiếp. Các kiểu nguyên thủy mà chúng ta đã thấy cho
đến nay triển khai Display
theo mặc định vì chỉ có một cách bạn muốn hiển thị
1
hoặc bất kỳ kiểu nguyên thủy nào khác cho người dùng. Nhưng với các struct,
cách println!
nên định dạng đầu ra ít rõ ràng hơn vì có nhiều khả năng hiển
thị hơn: Bạn có muốn dấu phẩy hay không? Bạn có muốn in dấu ngoặc nhọn? Tất cả
các trường có nên được hiển thị không? Do tính mơ hồ này, Rust không cố đoán
những gì chúng ta muốn, và các struct không có sẵn thực thi của Display
để sử
dụng với println!
và placeholder {}
.
Nếu chúng ta tiếp tục đọc các lỗi, chúng ta sẽ tìm thấy ghi chú hữu ích này:
= help: the trait `std::fmt::Display` is not implemented for `Rectangle`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
Hãy thử nó! Lệnh gọi macro println!
bây giờ sẽ trông như
println!("rect1 is {rect1:?}");
. Đặt đặc tả :?
bên trong dấu ngoặc nhọn báo
cho println!
rằng chúng ta muốn sử dụng một định dạng đầu ra được gọi là
Debug
. Trait Debug
cho phép chúng ta in struct theo cách hữu ích cho các nhà
phát triển để chúng ta có thể thấy giá trị của nó trong khi chúng ta đang gỡ lỗi
mã của mình.
Biên dịch mã với thay đổi này. Chết tiệt! Chúng ta vẫn nhận được một lỗi:
error[E0277]: `Rectangle` doesn't implement `Debug`
Nhưng một lần nữa, trình biên dịch cung cấp cho chúng ta một ghi chú hữu ích:
= help: the trait `Debug` is not implemented for `Rectangle`
= note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
Rust có bao gồm chức năng để in ra thông tin gỡ lỗi, nhưng chúng ta phải chọn
tham gia một cách rõ ràng để làm cho chức năng đó có sẵn cho struct của chúng
ta. Để làm điều đó, chúng ta thêm thuộc tính ngoài #[derive(Debug)]
ngay trước
định nghĩa struct, như được hiển thị trong Listing 5-12.
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!("rect1 is {rect1:?}"); }
Bây giờ khi chúng ta chạy chương trình, chúng ta sẽ không gặp bất kỳ lỗi nào, và chúng ta sẽ thấy đầu ra sau:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }
Tuyệt! Đây không phải là đầu ra đẹp nhất, nhưng nó hiển thị các giá trị của tất
cả các trường cho instance này, điều này chắc chắn sẽ giúp ích trong quá trình
gỡ lỗi. Khi chúng ta có các struct lớn hơn, sẽ hữu ích khi có đầu ra dễ đọc hơn
một chút; trong những trường hợp đó, chúng ta có thể sử dụng {:#?}
thay vì
{:?}
trong chuỗi println!
. Trong ví dụ này, sử dụng kiểu {:#?}
sẽ xuất ra
như sau:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle {
width: 30,
height: 50,
}
Một cách khác để in ra một giá trị sử dụng định dạng Debug
là sử dụng macro
dbg!
, lấy quyền sở hữu của một biểu thức (trái ngược với
println!
, lấy một tham chiếu), in ra tệp và số dòng nơi lệnh gọi macro dbg!
đó xuất hiện trong mã của bạn cùng với giá trị kết quả của biểu thức đó, và trả
lại quyền sở hữu của giá trị.
Lưu ý: Gọi macro
dbg!
in ra luồng bảng điều khiển lỗi tiêu chuẩn (stderr
), trái ngược vớiprintln!
, in ra luồng bảng điều khiển đầu ra tiêu chuẩn (stdout
). Chúng ta sẽ nói thêm vềstderr
vàstdout
trong phần "Viết Thông báo Lỗi cho Standard Error Thay vì Standard Output" trong Chương 12.
Đây là một ví dụ trong đó chúng ta quan tâm đến giá trị được gán cho trường
width
, cũng như giá trị của toàn bộ struct trong rect1
:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } fn main() { let scale = 2; let rect1 = Rectangle { width: dbg!(30 * scale), height: 50, }; dbg!(&rect1); }
Chúng ta có thể đặt dbg!
xung quanh biểu thức 30 * scale
và, bởi vì dbg!
trả lại quyền sở hữu của giá trị biểu thức, trường width
sẽ nhận được cùng một
giá trị như thể chúng ta không có lệnh gọi dbg!
ở đó. Chúng ta không muốn
dbg!
lấy quyền sở hữu của rect1
, vì vậy chúng ta sử dụng một tham chiếu đến
rect1
trong lệnh gọi tiếp theo. Đây là đầu ra của ví dụ này:
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10:16] 30 * scale = 60
[src/main.rs:14:5] &rect1 = Rectangle {
width: 60,
height: 50,
}
Chúng ta có thể thấy phần đầu tiên của đầu ra đến từ dòng 10 của src/main.rs
nơi chúng ta đang gỡ lỗi biểu thức 30 * scale
, và giá trị kết quả của nó là
60
(định dạng Debug
được thực hiện cho các số nguyên là chỉ in giá trị của
chúng). Lệnh gọi dbg!
trên dòng 14 của src/main.rs xuất ra giá trị của
&rect1
, đó là struct Rectangle
. Đầu ra này sử dụng định dạng Debug
đẹp của
kiểu Rectangle
. Macro dbg!
có thể thực sự hữu ích khi bạn đang cố gắng tìm
hiểu xem mã của bạn đang làm gì!
Ngoài trait Debug
, Rust đã cung cấp một số trait cho chúng ta sử dụng với
thuộc tính derive
có thể thêm hành vi hữu ích cho các kiểu tùy chỉnh của chúng
ta. Những trait đó và hành vi của chúng được liệt kê trong Phụ lục
C. Chúng ta sẽ đề cập đến cách triển khai các trait này
với hành vi tùy chỉnh cũng như cách tạo trait của riêng mình trong Chương 10.
Ngoài ra còn có nhiều thuộc tính khác ngoài derive
; để biết thêm thông tin,
xem phần "Thuộc tính" của Tham chiếu Rust.
Hàm area
của chúng ta rất cụ thể: nó chỉ tính diện tích của hình chữ nhật. Sẽ
hữu ích hơn nếu gắn hành vi này chặt chẽ hơn với struct Rectangle
của chúng ta
vì nó sẽ không hoạt động với bất kỳ kiểu nào khác. Hãy xem xét làm thế nào chúng
ta có thể tiếp tục cải tiến mã này bằng cách chuyển hàm area
thành phương
thức area
được định nghĩa trên kiểu Rectangle
của chúng ta.