Skip to main content

C5 使用 Struct

Rust 中 struct 为用于定义组合数据的自定义数据类型, 和面向对象语言中的 class 类似:

  1. struct 可以有关联函数, 类似类方法(class method).
  2. struct 可以有以 self(或 mut self/&self/&mut self) 为第一个参数的函数, 称为方法(method).
  3. struct 可以实现 trait, 类似实现接口.
  4. 但 struct 无法继承另外一个 struct, 只能包含另外一个 struct 作为成员.
  5. struct 和 enum 均有访问控制(pub, 否则默认 private)

此外, 在 rust 中还有 enum, 会在下一章详细看.

定义和初始化 struct

和 tuple 类似, struct 也用于组织不同类型的数据, 但和 tuple 不同的是, struct 需要为每个成员命名, 如下所示:

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

上述定义了一个名为 User 的 struct, 其中有四个成员(field), 且每个成员类型单独指定.

定义 struct 后, 要使用 struct, 需要首先进行初始化:

let user1 = User {
active: true,
username: String::from("someusername123"),
email: String::from("[email protected]"),
sign_in_count: 1,
};

初始化的默认语法即 struct 后跟随大括号并对每个成员进行初始化, 在没有实现 Default 的情况下(无法使用 ::default()), struct 初始化时必须对每个成员进行初始化(即赋值).

使用 . 访问 struct 的成员, 如: user1.active.

struct 初始化时, 若遇到和成员同名的变量, 可以进行简化形式的赋值:

fn build_user(email: String, username: String) -> User {
User {
active: true,
username, // 直接使用变量名
email, // 同上
sign_in_count: 1,
}
}

如果从同一类型的 struct 初始化新的 struct, 可以通过 .. 将现有 struct 成员展开到新建的 struct:

let user2 = User {
email: String::from("[email protected]"), // 不同的成员单独赋值
..user1 // 将之前的 user1 展开到新建的 struct 中
};

不过, 当使用展开语法的时候需要注意: 现有 struct 的成员被展开到新 struct, 实际是成员的值被 move 到新 struct(对于非 Copy 成员而言). 结果就是这些被 move 的成员将无法再次通过现有 struct 访问:

let u2 = User {
email: "[email protected]".to_string(),
..u1
};

// 如下三个成员在后续访问时:

u1.email; // Ok 可以在 u1 上访问, 因为 email 没有被 move, u2 中 email 是新建的.
u1.user_name; // Error! 无法在 u1 上访问, 因为 user_name 被 move 到了 u2 中.
u1.sign_in_count; // Ok 是 Copy 类型的, 不存在 move 可能性.

特殊 struct 形式

有如下几种:

  1. Tuple Struct: 即不对成员命名的 struct, 比如:

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

    // Color 和 Point 是不同类型的 struct, 即便它们的成员一致. 这点和 tuple 不一样.
    let c = Color(10, 255, 0);
    let p = Point(1, 2, 1);

    // 需要使用 `.0`, `.1` 类型下标语法访问其成员
    c.0 // 10
    c.1 // 255

    Tuple Struct 除了上述用途外, 还可以在 FFI 场景下对不同的 C 透明指针(void*)进行区分表示, 让代码更易读.

  2. Unit Struct: 即没有任何成员的 struct, 比如:

    struct AlwaysEqual; // Unit Struct

    let s = AlwaysEqual; // Unit Struct 的初始化

    其用途主要是: 当想实现一个 trait, 但不需要包含任何 struct 成员时.

Struct 成员的 Ownership

上面已经看到了 struct 成员是可以被 move 出 struct 的, 当 move 出去后就无法再在原 struct 上访问了.

struct 成员可以是任何类型的, 当然也就可以是引用类型的, 当 struct 成员是引用类型时, 需要为其成员指定生命期标记. 作用是为了保证引用成员在访问时一直是合法的(至少引用成员引用的内容应和 struct 存在一样久). 关于 Lifetime 的详细讲解会放到 C10.

使用 Derived Trait 为 Struct 添加功能

可以使用 outer attribute 中的 #[derive()] 为 struct 添加 trait 的默认实现, 比如:

#[derive(Debug)] // 获得 Debug 默认实现.
struct Color {
red: i32,
green: i32,
blue: i32
}

关于 Derivable Traits, Rust 提供了许多, 详见这个列表

一些小细节

println! 宏中的一些格式化控制:

  • {}: 使用 Display trait 的输出作为打印内容
  • {:?}: 使用 Debug trait 的输出作为打印内容
  • {:#?}: 使用更易读的 Debug 输出作为打印内容(pretty Debug)

其他:

  • println! 的输出为 stdout, 传入被输出内容的引用.
  • dbg! 的输出为 stderr, 会获取被输出内容的 Ownership, 完成后再归还.

实现 struct 方法

实现 struct 方法的语法如下:

struct Color {
// ...
}

impl Color { // 关联函数及method 实现
// 关联函数
fn new_color() -> Self {
//...
}

// method
fn is_dark(&self) -> bool {
//...
}
}

impl Debug for Color { // 实现一个 trait
// ...
}

其中:

  1. &self 实际是 self: &Self 的简写, 而 Self 表示的是这个 impl 所实现的类型, 这里就是 Color.
  2. &self 为了 borrow self, 而不会将 self move 进来. 要 move self 进来, 就直接使用 self 作为第一个参数. (move self 的方法一般是用于要做某种变换的时候, 场景不多).
  3. 同理, 要改变 self, 可以使用 mut self&mut self
  4. 方法调用时, 一般不需要写 (&c).is_dark(), 因为 rust 会自动根据方法第一个参数的类型进行补全(&, &mut, * 等等).
  5. 关联函数调用使用, 使用 :: 符进行.