Bootstrap

Rust 中的结构体使用指南

1. 定义结构体(Struct)

定义一个结构体需要使用 struct 关键字,然后给该结构体取一个有意义的名字,接着在花括号内列出字段名称和字段类型。例如,下面的 User 结构体包含四个字段,用来表示用户的相关信息:

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

需要注意的是,结构体字段之间用逗号分隔,字段类型可以各不相同。字段名有助于我们理解各数据的含义,相比于元组,这种方式更具可读性和可维护性。

2. 实例化结构体

定义完结构体后,我们就可以基于它创建具体的实例。创建实例需要在结构体名称后面的花括号中填写 字段: 值。例如:

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

上述代码创建了一个名为 user1User 实例。

  • 访问字段时,使用点号(.)语法,比如 user1.email
  • 如果想要对结构体的字段进行修改,需要将整个实例声明为可变(mut),如下所示:
let mut user1 = User {
    active: true,
    username: String::from("someusername123"),
    email: String::from("[email protected]"),
    sign_in_count: 1,
};

// 修改 email
user1.email = String::from("[email protected]");

Rust 不允许只将部分字段设为可变;只有把整个实例设为 mut,才能修改任意字段的值。

3. 返回结构体实例的函数

我们可以编写一个函数来返回一个结构体实例。以下例子中,build_user 函数接收用户名和邮箱,返回一个 User 实例:

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

这其中有一些重复:结构体字段和函数参数名字完全相同时,写起来显得冗余。Rust 提供了字段初始化简写语法(field init shorthand) 来简化这种情况:

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

当字段名和变量名相同时,可以直接省略 username: username,只写 username

4. 从已有实例更新字段:结构体更新语法

有时我们想根据现有的结构体实例创建一个新实例,只修改其中一些字段,而其余字段沿用旧实例的值。使用“结构体更新语法”(struct update syntax)能让我们少写大量重复代码。先看常规写法:

// 常规写法:手动为所有字段赋值
let user2 = User {
    active: user1.active,
    username: String::from("new_user"),
    email: String::from("[email protected]"),
    sign_in_count: user1.sign_in_count,
};

使用更新语法,只需要在花括号最后使用 .. 加上已存在的实例名称:

let user2 = User {
    email: String::from("[email protected]"),
    // 其他未指定的字段全部拷贝自 user1
    ..user1
};

这样就将 activeusernamesign_in_count 字段从 user1 中拷贝了过来,而 email 则使用了新的值。

需要注意的是,这种用法会发生所有权转移move)。如果被转移的字段(如 String)在新实例 user2 中使用了 user1 的原始值,那么 user1 中对应的字段不再有效。如果被转移的字段类型实现了 Copy trait(如 u64),则不会失效,因为它是按值拷贝的。

5. 元组结构体(Tuple Struct)

Rust 还支持一种“元组结构体”(tuple struct),其形式与常规结构体类似,只是不为字段命名,而是直接在结构体名后面写上字段类型,如同元组一样。例如:

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

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

    println!("Color: {}, {}, {}", black.0, black.1, black.2);
    println!("Point: {}, {}, {}", origin.0, origin.1, origin.2);
}
  • blackorigin 虽然都含有三个 i32,但它们属于不同的类型:ColorPoint
  • 这种设计可以让你在概念上区分“颜色”和“坐标点”,即使它们的内部结构相同。

6. 类单元结构体(Unit-Like Struct)

Rust 允许定义没有任何字段的结构体,被称为类单元结构体(unit-like struct)。它的用法和 ()(unit 类型)非常类似。比如:

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

这里的 AlwaysEqual 并没有任何字段,但是将来可以为它实现某些特定的 trait,使得其任意两个实例都“总是相等”(或者做其他自定义的行为)。这种结构体常用于在类型系统层面上代表某种独特概念,或者在泛型、trait 中提供占位符作用。

7. 结构体中的所有权问题

在上面的 User 例子里,我们使用了 String 而不是 &str 作为字段类型。这是因为我们希望这个结构体拥有自己的数据,并且在结构体生命周期内字段数据一直有效。如果结构体内部要存储引用(如 &str),则需要用到生命周期(lifetime) 来保证被引用的数据在结构体作用范围内不被释放。
如果你试图直接在结构体中写:

struct User {
    active: bool,
    username: &str, // 会出错
    email: &str,    // 会出错
    sign_in_count: u64,
}

编译器会提示需要生命周期标注(lifetime specifier),否则无法保证引用的有效性。关于如何在结构体中使用引用,以及如何正确指定生命周期,我们会在更高级的章节(通常是第 10 章)做更深入的讨论。

;