键盘快捷键

按 ← 或 → 在章节间切换

按 S 或 / 搜索本书

按 ? 显示此帮助

按 Esc 隐藏此帮助

定义和实例化结构体

结构体与元组类似,二者都可以保存多个相关值(元组已在“元组类型”一节中介绍)。和元组一样,结构体中的各个部分可以是不同类型。不同之处在于,结构体会为每项数据命名,让值的含义一目了然。由于有了这些名称,结构体比元组更灵活:指定或访问实例中的值时,不必依赖数据的顺序。

定义结构体时,我们使用关键字 struct 并为整个结构体命名。结构体的名称应描述组合在一起的数据所表达的含义。然后,在花括号内定义各项数据的名称和类型,这些数据称为字段。例如,示例 5-1 展示了一个用于存储用户账户信息的结构体。

文件名:src/main.rs
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64,
}

fn main() {}
示例 5-1:User 结构体定义

定义结构体后,要使用它,我们需要为每个字段指定具体值,从而创建该结构体的一个实例。创建实例时,先写出结构体名称,再在花括号中添加键: 值对;键是字段名称,值是要存储在字段中的数据。字段的指定顺序不必与它们在结构体定义中的声明顺序相同。换句话说,结构体定义是该类型的通用模板,实例则用具体数据填充模板,以创建该类型的值。例如,我们可以像示例 5-2 那样声明一个具体用户。

文件名:src/main.rs
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,
    };
}
示例 5-2:创建一个 User 结构体实例

要获取结构体中的特定值,可以使用点号表示法。例如,要访问该用户的电子邮件地址,可以写 user1.email。如果实例是可变的,就可以使用点号表示法并向特定字段赋值来更改其值。示例 5-3 展示了如何更改可变 User 实例的 email 字段。

文件名:src/main.rs
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");
}
示例 5-3:更改 User 实例 email 字段的值

请注意,整个实例都必须是可变的;Rust 不允许只将某些字段标记为可变。与其他表达式一样,我们可以在函数体的最后一个表达式中构造一个新的结构体实例,从而隐式返回该实例。

示例 5-4 展示了一个 build_user 函数,它接收给定的电子邮件地址和用户名,并返回一个 User 实例。active 字段的值为 true,sign_in_count 字段的值为 1。

文件名:src/main.rs
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"),
    );
}
示例 5-4:接收电子邮件地址和用户名并返回 User 实例的 build_user 函数

让函数参数与结构体字段同名很自然,但重复书写 email 和 username 字段名及变量名有些繁琐。如果结构体字段更多,逐一重复名称就更令人厌烦了。好在有一种方便的简写语法!

使用字段初始化简写

在示例 5-4 中,参数名称与结构体字段名称完全相同,因此我们可以使用字段初始化简写语法改写 build_user,使其行为完全相同,同时避免重复书写 username 和 email,如示例 5-5 所示。

文件名:src/main.rs
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"),
    );
}
示例 5-5:由于 username 和 email 参数与结构体字段同名,build_user 函数使用了字段初始化简写

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

使用结构体更新语法创建实例

有时,我们需要创建一个新的结构体实例,它的大部分值来自另一个同类型实例,但其中一些值有所不同。这时可以使用结构体更新语法。

首先,示例 5-6 展示了不使用更新语法、以常规方式在 user2 中创建一个新的 User 实例。我们为 email 设置了新值,其余字段则使用示例 5-2 中创建的 user1 的值。

文件名:src/main.rs
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,
    };
}
示例 5-6:创建一个新的 User 实例,其中除一个值外,其余值都取自 user1

如示例 5-7 所示,使用结构体更新语法可以用更少的代码实现相同效果。语法 .. 表示未显式设置的其余字段应与给定实例中的对应字段具有相同的值。

文件名:src/main.rs
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
    };
}
示例 5-7:使用结构体更新语法为 User 实例设置新的 email 值,其余值则取自 user1

示例 5-7 的代码还在 user2 中创建了一个实例:它的 email 值不同,但 username、active 和 sign_in_count 字段的值与 user1 相同。..user1 必须放在最后,用来指定所有剩余字段都从 user1 的对应字段获取值;不过,我们可以按任意顺序为任意多个字段指定值,无须遵循结构体定义中的字段顺序。

注意,结构体更新语法像赋值一样使用 =;这是因为它会移动数据,正如“变量与数据的交互:移动”一节所述。在这个例子中,创建 user2 后就不能再使用 user1,因为 user1 的 username 字段中的 String 已被移动到 user2。如果我们为 user2 的 email 和 username 都提供新的 String 值,只从 user1 使用 active 和 sign_in_count,那么创建 user2 后 user1 仍然有效。active 和 sign_in_count 都是实现了 Copy trait 的类型,因此适用“只存储在栈上的数据:Copy”一节中讨论的行为。这个例子中仍然可以使用 user1.email,因为它的值没有从 user1 中移出。

使用元组结构体创建不同类型

Rust 还支持一种外形类似元组的结构体,称为元组结构体。元组结构体拥有结构体名称所赋予的含义,但字段没有关联的名称,只有字段类型。当你想为整个元组命名,使它成为区别于其他元组的类型,或者像普通结构体那样为每个字段命名显得冗长或多余时,元组结构体就很有用。

定义元组结构体时,先写关键字 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 的类型不同,因为它们分别是不同元组结构体的实例。每个结构体定义都是独立的类型,即使其中的字段类型可能相同。例如,接收 Color 类型参数的函数不能将 Point 作为参数,尽管这两种类型都由三个 i32 值组成。除此之外,元组结构体实例与元组类似,可以将其解构为各个部分,也可以使用 . 后跟索引来访问单个值。与元组不同,解构元组结构体时必须写出结构体类型名称。例如,可以写 let Point(x, y, z) = origin;,将 origin 点中的值解构到名为 x、y 和 z 的变量中。

定义类单元结构体

你也可以定义不含任何字段的结构体!它们称为类单元结构体,因为其行为类似于单元类型 (),我们已在“元组类型”一节中提到过。当你需要为某种类型实现 trait、但不需要在该类型本身存储任何数据时,类单元结构体会很有用。我们将在第 10 章讨论 trait。下面是声明并实例化名为 AlwaysEqual 的类单元结构体的示例:

文件名:src/main.rs
struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

定义 AlwaysEqual 时,使用关键字 struct、所需的名称,然后加上分号即可。不需要花括号或圆括号!之后,我们也可以用类似的方式在 subject 变量中获取 AlwaysEqual 实例:只写已定义的名称,不加花括号或圆括号。设想我们稍后为这种类型实现某种行为,使每个 AlwaysEqual 实例都始终与其他任何类型的实例相等,也许这样测试时就能得到确定的结果。实现这种行为并不需要任何数据!你将在第 10 章看到如何定义 trait,并为包括类单元结构体在内的任何类型实现 trait。

结构体数据的所有权

在示例 5-1 的 User 结构体定义中,我们使用了拥有所有权的 String 类型,而不是 &str 字符串切片类型。这是有意为之,因为我们希望该结构体的每个实例都拥有自己的全部数据,并且只要整个结构体有效,这些数据就始终有效。

结构体也可以存储对其他对象所拥有数据的引用,但这需要使用生命周期,这是 Rust 的一项特性,我们将在第 10 章讨论。生命周期确保结构体引用的数据在结构体有效期间也始终有效。假设你尝试在结构体中存储引用,却没有指定生命周期,就像下面 src/main.rs 中的代码一样;这样做行不通:

文件名: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 2 previous errors

在第 10 章中,我们会讨论如何修复这些错误,从而让你可以在结构体中存储引用;但目前,我们会使用 String 这样的拥有所有权的类型,而不是 &str 这样的引用,来避免此类错误。