Định nghĩa và Khởi tạo Struct

Struct tương tự như tuple, đã được thảo luận trong phần "Kiểu Tuple", ở chỗ cả hai đều chứa nhiều giá trị liên quan. Giống như tuple, các thành phần của một struct có thể có kiểu khác nhau. Khác với tuple, trong một struct, bạn sẽ đặt tên cho từng phần dữ liệu để làm rõ ý nghĩa của các giá trị. Việc thêm các tên này có nghĩa là struct linh hoạt hơn tuple: bạn không phải dựa vào thứ tự của dữ liệu để xác định hoặc truy cập các giá trị của một thực thể.

Để định nghĩa một struct, chúng ta nhập từ khóa struct và đặt tên cho toàn bộ struct. Tên của struct nên mô tả ý nghĩa của các phần dữ liệu được nhóm lại với nhau. Sau đó, bên trong dấu ngoặc nhọn, chúng ta định nghĩa tên và kiểu của các phần dữ liệu, mà chúng ta gọi là trường (fields). Ví dụ, Listing 5-1 hiển thị một struct lưu trữ thông tin về tài khoản người dùng.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {}

Để sử dụng struct sau khi đã định nghĩa nó, chúng ta tạo ra một instance (thực thể) của struct đó bằng cách xác định giá trị cụ thể cho mỗi trường. Chúng ta tạo một thực thể bằng cách nêu tên của struct và sau đó thêm dấu ngoặc nhọn chứa các cặp key: value, trong đó key là tên của các trường và value là dữ liệu mà chúng ta muốn lưu trữ trong các trường đó. Chúng ta không cần phải chỉ định các trường theo cùng thứ tự mà chúng ta đã khai báo trong struct. Nói cách khác, định nghĩa struct giống như một khuôn mẫu chung cho kiểu dữ liệu, và các thực thể điền vào khuôn mẫu đó với dữ liệu cụ thể để tạo ra các giá trị của kiểu đó. Ví dụ, chúng ta có thể khai báo một người dùng cụ thể như trong Listing 5-2.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };
}

Để lấy một giá trị cụ thể từ struct, chúng ta sử dụng ký hiệu dấu chấm. Ví dụ, để truy cập địa chỉ email của người dùng này, chúng ta sử dụng user1.email. Nếu thực thể là có thể thay đổi, chúng ta có thể thay đổi giá trị bằng cách sử dụng ký hiệu dấu chấm và gán vào một trường cụ thể. Listing 5-3 cho thấy cách thay đổi giá trị trong trường email của một thực thể User có thể thay đổi.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    let mut user1 = User {
        active: true,
        username: String::from("someusername123"),
        email: String::from("someone@example.com"),
        sign_in_count: 1,
    };

    user1.email = String::from("anotheremail@example.com");
}

Lưu ý rằng toàn bộ thực thể phải là có thể thay đổi; Rust không cho phép chúng ta đánh dấu chỉ một số trường nhất định là có thể thay đổi. Giống như với bất kỳ biểu thức nào, chúng ta có thể tạo một thực thể mới của struct làm biểu thức cuối cùng trong thân hàm để ngầm định trả về thực thể mới đó.

Listing 5-4 hiển thị một hàm build_user trả về một thực thể User với email và tên người dùng đã cho. Trường active nhận giá trị là true, và sign_in_count nhận giá trị là 1.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username: username,
        email: email,
        sign_in_count: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}

Việc đặt tên các tham số của hàm giống với tên trường của struct là hợp lý, nhưng việc phải lặp lại tên trường emailusername cùng với biến là hơi tẻ nhạt. Nếu struct có nhiều trường hơn, việc lặp lại mỗi tên sẽ càng phiền phức hơn. May mắn thay, có một cách viết tắt thuận tiện!

Sử dụng Field Init Shorthand

Vì tên tham số và tên trường struct là hoàn toàn giống nhau trong Listing 5-4, chúng ta có thể sử dụng cú pháp field init shorthand để viết lại build_user để nó hoạt động chính xác như cũ nhưng không có sự lặp lại của usernameemail, như trong Listing 5-5.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn build_user(email: String, username: String) -> User {
    User {
        active: true,
        username,
        email,
        sign_in_count: 1,
    }
}

fn main() {
    let user1 = build_user(
        String::from("someone@example.com"),
        String::from("someusername123"),
    );
}

Ở đây, chúng ta đang tạo một thực thể mới của struct User, nó có một trường tên là email. Chúng ta muốn đặt giá trị của trường email thành giá trị trong tham số email của hàm build_user. Vì trường email và tham số email có cùng tên, chúng ta chỉ cần viết email thay vì email: email.

Tạo Thực thể từ Các Thực thể Khác với Cú pháp Cập nhật Struct

Thường rất hữu ích khi tạo một thực thể mới của một struct bao gồm hầu hết các giá trị từ một thực thể khác của cùng kiểu, nhưng thay đổi một số. Bạn có thể làm điều này bằng cách sử dụng cú pháp cập nhật struct (struct update syntax).

Đầu tiên, trong Listing 5-6, chúng ta thấy cách tạo một thực thể User mới trong user2 theo cách thông thường, không sử dụng cú pháp cập nhật. Chúng ta đặt một giá trị mới cho email nhưng sử dụng các giá trị giống nhau từ user1 mà chúng ta đã tạo ra trong Listing 5-2.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("another@example.com"),
        sign_in_count: user1.sign_in_count,
    };
}

Sử dụng cú pháp cập nhật struct, chúng ta có thể đạt được cùng kết quả với ít mã hơn, như trong Listing 5-7. Cú pháp .. chỉ định rằng các trường còn lại không được đặt rõ ràng sẽ có cùng giá trị với các trường trong thực thể đã cho.

struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {
    // --snip--

    let user1 = User {
        email: String::from("someone@example.com"),
        username: String::from("someusername123"),
        active: true,
        sign_in_count: 1,
    };

    let user2 = User {
        email: String::from("another@example.com"),
        ..user1
    };
}

Mã trong Listing 5-7 cũng tạo ra một thực thể trong user2 có giá trị khác cho email nhưng có cùng giá trị cho các trường username, active, và sign_in_count từ user1. ..user1 phải đặt cuối cùng để chỉ định rằng bất kỳ trường còn lại nào nên lấy giá trị của chúng từ các trường tương ứng trong user1, nhưng chúng ta có thể chọn chỉ định giá trị cho nhiều trường tùy ý theo bất kỳ thứ tự nào, bất kể thứ tự của các trường trong định nghĩa struct.

Lưu ý rằng cú pháp cập nhật struct sử dụng = như một phép gán; điều này là vì nó di chuyển dữ liệu, giống như chúng ta đã thấy trong phần "Variables and Data Interacting with Move". Trong ví dụ này, chúng ta không còn có thể sử dụng user1 sau khi tạo user2String trong trường username của user1 đã được di chuyển vào user2. Nếu chúng ta đã cung cấp cho user2 giá trị String mới cho cả emailusername, và do đó chỉ sử dụng các giá trị activesign_in_count từ user1, thì user1 vẫn sẽ hợp lệ sau khi tạo ra user2. Cả activesign_in_count đều là các kiểu thực hiện trait Copy, do đó hành vi mà chúng ta đã thảo luận trong phần "Stack-Only Data: Copy" sẽ được áp dụng. Chúng ta cũng vẫn có thể sử dụng user1.email trong ví dụ này, vì giá trị của nó không bị di chuyển ra khỏi user1.

Sử dụng Tuple Struct Không Có Trường Được Đặt Tên để Tạo Các Kiểu Khác Nhau

Rust cũng hỗ trợ struct trông giống như tuple, được gọi là tuple struct. Tuple struct có thêm ý nghĩa mà tên struct cung cấp nhưng không có tên được liên kết với các trường của chúng; thay vào đó, chúng chỉ có các kiểu của các trường. Tuple struct hữu ích khi bạn muốn đặt tên cho toàn bộ tuple và làm cho tuple đó thành một kiểu khác với các tuple khác, và khi đặt tên cho mỗi trường như trong một struct thông thường sẽ dài dòng hoặc thừa thãi.

Để định nghĩa một tuple struct, bắt đầu với từ khóa struct và tên struct theo sau là các kiểu trong tuple. Ví dụ, ở đây chúng ta định nghĩa và sử dụng hai tuple struct có tên là ColorPoint:

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

fn main() {
    let black = Color(0, 0, 0);
    let origin = Point(0, 0, 0);
}

Lưu ý rằng giá trị blackorigin là các kiểu khác nhau vì chúng là các thực thể của các tuple struct khác nhau. Mỗi struct bạn định nghĩa là kiểu riêng của nó, ngay cả khi các trường trong struct có thể có cùng kiểu. Ví dụ, một hàm nhận một tham số kiểu Color không thể nhận một Point làm đối số, mặc dù cả hai kiểu đều được tạo thành từ ba giá trị i32. Nếu không, các thực thể tuple struct tương tự như tuple ở chỗ bạn có thể phân rã chúng thành các phần riêng lẻ, và bạn có thể sử dụng . theo sau bởi chỉ số để truy cập một giá trị riêng lẻ. Khác với tuple, tuple struct yêu cầu bạn đặt tên kiểu của struct khi bạn phân rã chúng. Ví dụ, chúng ta sẽ viết let Point(x, y, z) = origin; để phân rã các giá trị trong điểm origin thành các biến có tên là x, y, và z.

Unit-Like Struct Không Có Trường Nào

Bạn cũng có thể định nghĩa struct không có trường nào! Những thứ này được gọi là unit-like struct vì chúng hoạt động tương tự như (), kiểu đơn vị mà chúng ta đã đề cập trong phần "The Tuple Type". Unit-like struct có thể hữu ích khi bạn cần thực hiện một trait trên một kiểu nhưng không có bất kỳ dữ liệu nào mà bạn muốn lưu trữ trong chính kiểu đó. Chúng ta sẽ thảo luận về trait trong Chương 10. Đây là một ví dụ về việc khai báo và khởi tạo một unit struct có tên là AlwaysEqual:

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

Để định nghĩa AlwaysEqual, chúng ta sử dụng từ khóa struct, tên mà chúng ta muốn, và sau đó là dấu chấm phẩy. Không cần dấu ngoặc nhọn hoặc dấu ngoặc tròn! Sau đó chúng ta có thể lấy một thực thể của AlwaysEqual trong biến subject theo cách tương tự: sử dụng tên mà chúng ta đã định nghĩa, không có dấu ngoặc nhọn hoặc dấu ngoặc tròn nào. Hãy tưởng tượng rằng sau này chúng ta sẽ thực hiện hành vi cho kiểu này sao cho mọi thực thể của AlwaysEqual luôn bằng với mọi thực thể của bất kỳ kiểu nào khác, có lẽ để có một kết quả đã biết cho mục đích kiểm tra. Chúng ta sẽ không cần bất kỳ dữ liệu nào để thực hiện hành vi đó! Bạn sẽ thấy trong Chương 10 cách định nghĩa trait và thực hiện chúng trên bất kỳ kiểu nào, bao gồm cả unit-like struct.

Quyền sở hữu Dữ liệu trong Struct

Trong định nghĩa struct User ở Listing 5-1, chúng ta đã sử dụng kiểu String có quyền sở hữu thay vì kiểu string slice &str. Đây là một lựa chọn có chủ đích bởi vì chúng ta muốn mỗi thực thể của struct này sở hữu tất cả dữ liệu của nó và cho dữ liệu đó hợp lệ miễn là toàn bộ struct còn hợp lệ.

Cũng có thể để struct lưu trữ các tham chiếu đến dữ liệu thuộc sở hữu của một thứ khác, nhưng để làm điều đó đòi hỏi phải sử dụng lifetimes, một tính năng của Rust mà chúng ta sẽ thảo luận trong Chương 10. Lifetimes đảm bảo rằng dữ liệu được tham chiếu bởi một struct là hợp lệ miễn là struct còn tồn tại. Hãy giả sử bạn cố gắng lưu trữ một tham chiếu trong một struct mà không chỉ định lifetimes, như sau; điều này sẽ không hoạt động:

struct User {
    active: bool,
    username: &str,
    email: &str,
    sign_in_count: u64,
}

fn main() {
    let user1 = User {
        active: true,
        username: "someusername123",
        email: "someone@example.com",
        sign_in_count: 1,
    };
}

Trình biên dịch sẽ phàn nàn rằng nó cần bộ chỉ định lifetime:

$ cargo run
   Compiling structs v0.1.0 (file:///projects/structs)
error[E0106]: missing lifetime specifier
 --> src/main.rs:3:15
  |
3 |     username: &str,
  |               ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 ~     username: &'a str,
  |

error[E0106]: missing lifetime specifier
 --> src/main.rs:4:12
  |
4 |     email: &str,
  |            ^ expected named lifetime parameter
  |
help: consider introducing a named lifetime parameter
  |
1 ~ struct User<'a> {
2 |     active: bool,
3 |     username: &str,
4 ~     email: &'a str,
  |

For more information about this error, try `rustc --explain E0106`.
error: could not compile `structs` (bin "structs") due to 2 previous errors

Trong Chương 10, chúng ta sẽ thảo luận về cách sửa những lỗi này để bạn có thể lưu trữ tham chiếu trong struct, nhưng hiện tại, chúng ta sẽ sửa các lỗi như thế này bằng cách sử dụng các kiểu có quyền sở hữu như String thay vì các tham chiếu như &str.