rust-数据结构

发布于:2025-07-25 ⋅ 阅读:(17) ⋅ 点赞:(0)

定义和实例化结构体

结构体类似于“元组类型”一节中讨论的元组,因为两者都包含多个相关的值。像元组一样,结构体中的各个部分可以是不同的类型。但与元组不同的是,在结构体中你需要为每个数据部分命名,以便明确这些值的含义。添加这些名称使得结构体比元组更灵活:你不必依赖数据的顺序来指定或访问实例中的值。

要定义一个结构体,我们输入关键字 struct 并为整个结构体命名。结构体的名称应描述被组合在一起的数据部分的重要性。然后,在花括号内定义数据部分(称为字段)的名称和类型。例如,清单 5-1 展示了一个存储用户账户信息的结构体。

文件名:src/main.rs

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

清单 5-1:User 结构体定义
在定义好一个结构体后,要使用它,我们通过为每个字段指定具体值来创建该结构体的实例。我们通过写出该结构体名称,然后加上包含键值对(key: value)的花括号来创建实例,其中键是字段名,值是我们想存储在这些字段中的数据。我们不必按照声明时的顺序指定字段。换句话说,结构体验证就像一种通用模板,而实例则用特定的数据填充这个模板,从而创建该类型的具体数值。例如,我们可以如清单 5-2 所示声明一个特定用户。
文件名: src/main.rs

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");
}

清单 5-3:修改 User 实例中 email 字段的值
注意,整个实例必须是可变的;Rust 不允许我们只标记某些字段为可变。与任何表达式一样,我们可以在函数体的最后一个表达式中构造结构体的新实例,以隐式返回该新实例。
清单 5-4 展示了一个 build_user 函数,它返回一个带有给定 email 和 username 的 User 实例。active 字段被赋值为 true,sign_in_count 被赋值为 1。
文件名: src/main.rs

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

清单 5-4:一个接受 email 和 username 并返回 User 实例的 build_user 函数
将函数参数命名为与结构体字段相同的名字是合理的,但重复写出 email 和 username 字段名称及变量有点繁琐。如果结构体有更多字段,重复每个名字会更加烦人。幸运的是,有一种方便的简写方式!

使用字段初始化简写

因为参数名和结构体字段名在清单 5-4 中完全相同,我们可以使用字段初始化简写语法重写 build_user,使其行为完全相同,但不必重复书写 username 和 email,如清单 5-5 所示。

文件名:src/main.rs

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

清单 5-5:一个使用字段初始化简写的 build_user 函数,因为 username 和 email 参数与结构体字段同名

这里我们创建了一个新的 User 实例,它有一个名为 email 的字段。我们想将该 email 字段的值设置为 build_user 函数中 email 参数的值。由于 email 字段和参数名称相同,我们只需写成 email,而不是 email: email

用结构体更新语法从其他实例创建新实例

通常需要基于另一个实例的大部分值来创建新实例,但修改其中一些值。这时可以使用结构体更新语法。

首先,在清单 5-6 中展示了如何常规地(不使用更新语法)创建 user2 新的 User 实例。我们给 email 设置了新值,其余则沿用了之前在清单 5-2 创建的 user1 的对应值。

文件名:src/main.rs

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

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

清单 5-6:用除了一个之外全部来自 user1 值的新建 User 实例

利用结构体更新语法,可以用更少代码实现相同效果,如清单 5-7 所示。.. 表示未显式设置的剩余字段应取自指定实例中的对应字段。

文件名:src/main.rs

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

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

清单 5-7:用结构体更新语法为 User 实例设定新的邮箱,同时其它所有值都来自于 user1

如上所示,user2 拥有不同的邮箱,但用户名、active 和 sign_in_count 等其它字段均继承自 user1。..user1 必须放最后,用以指明剩余未赋值的字段从哪个实例获取。但你可按任意顺序指定任意数量的具体字段,无需遵循结构定义中各个域出现顺序。

注意,结构体更新语法看起来像赋值操作符 =;这是因为它会移动数据,就像“变量与数据交互中的移动”章节讲到的一样。在本例中,由于用户名字符串被移入到了 new 用户,所以不能再继续使用原来的 user1 。如果给 user2 提供了新的字符串类型邮箱和用户名,只复用了 activesign_in_count 两个实现 Copy trait 的类型,那么在构造完后仍然能继续有效地访问原来的 user1 。这两个类型属于栈上复制的数据,因此适用于“仅限栈上的数据拷贝”章节讨论过的方法。本例还能访问到原来用户里的邮件地址,因为它没有被移动出去。

无命名域元组结构用于区分不同类型

Rust 同样支持类似元组但带名字标识符号称作元组结构(tuple structs)。元组结构拥有额外含义,即由其名字提供,但其内部元素没有独立命名,仅包含元素类型。当你希望整体给这个元组起个名字,并使得此类元组区别于其他普通元组时非常有用,也适合当每个成员逐一定义名称显得冗长或多余时采用。

定义方式是先声明 struct,再跟上名称及括号内列出的各元素类型。例如,这里定义并且使用两个分别叫 Color 和 Point 的元组结构:

文件名:src/main.rs

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

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

请注意,black 与 origin 是不同类型,因为它们分别是不同元组结 构(struct) 类型下生成出来的对象。每种 struct 都是独立的新类型,即便里面成员都是一样的数据型别。例如,一个函数若要求传入 Color 类型,则不能传入 Point 类型作为实参,即使两者都由三个 i32 数字组成。此外,虽然 tuple struct 在解构拆包以及通过索引点位访问方面类似普通 tuple,不同的是解构时必须明确指出要解构的是哪种 tuple struct,比如要这样写:

let Point(x, y, z) = point;

无任何字段的类单元(struct Unit-Like Structs)

你还可以定义不包含任何字段的 struct!这些被称作类单元素(struct unit-like structs),因为它们行为类似于 () 单位值(在“Tuple Type”章节中提到过)。当你需要对某种类型实现 trait,但不需要在该类型本身存储任何数据时,类单元素很有用。我们将在第10章讨论 trait。这是声明和实例化一个名为 AlwaysEqual 的类单元素示例:

文件名:src/main.rs

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

定义 AlwaysEqual 时,我们使用了 struct 关键字、所需名称,然后以分号结尾。不需要花括号或圆括号!然后我们就能像这样获取 AlwaysEqual 实例:直接用已定义好的名字,不带花括号或圆括号。假设以后我们会实现这个类型的一些行为,使得所有 AlwaysEqual 实例总是等价于任意其他实例,比如用于测试目的且结果可预知。这种行为无需存储任何数据即可实现!第10章将介绍如何定义 trait 并将其应用到包括类单元素在内任意类型上。

Struct 数据所有权

在清单5-1中的 User struct 定义里,我们用了拥有所有权(owned)的 String 类型,而非字符串切片 &str。这是刻意选择,因为希望每个该 struct 实例拥有自身全部数据,并保证这些数据只要整个 struct 有效就一直有效。

当然,也可以让 structs 存储指向其它地方拥有的数据引用,但这要求使用生命周期(lifetimes),这是 Rust 的一项特性,第10章会详细讲解。生命周期确保被引用的数据只要对应 struct 有效就保持有效。如果尝试在未指定生命周期情况下,将引用存入 struct,如下代码,则无法编译:

文件名:src/main.rs

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,
    };
}

编译器会报错提示缺少生命周期说明符:

$ 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 previous errors.

第10章中我们将讲述如何修复此错误,以便能够把引用存入 structs;但目前,我们通过改用拥有所有权的数据如 String 来替代引用(&str),从而避免此类错误。


网站公告

今日签到

点亮在社区的每一天
去签到