定义和实例化结构体

结构体类似于元组,它们都在 “元组类型” 部分中讨论过,两者都可以包含多个相关联的值。和元组一样,结构体的组成部分可以是不同类型的。与元组不同的是,在结构体中,你需要命名每一部分数据,这样就能清楚地知道这些值代表什么。添加这些名称意味着结构体比元组更灵活:你不需要依赖数据的顺序来指定或访问某个实例的值。

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

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

fn main() {}
清单 5-1:一个 User 结构体定义

定义结构体后,要使用它,需要通过为每个字段指定具体的值来创建该结构体的 实例(instance)。我们通过指定结构体的名称,然后加上包含 键: 值 对的大括号来创建实例,其中键是字段的名称,值是我们希望存储在这些字段中的数据。我们不必按照在结构体中声明字段的顺序来指定字段。换句话说,结构体定义就像是类型的通用模板,而实例则用特定的数据填充该模板来创建该类型的值。例如,我们可以像清单 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 字段的值为 truesign_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 函数

将函数参数与结构体字段命名相同是有道理的,但不得不重复 emailusername 字段名和变量名有点繁琐。如果结构体有更多字段,重复每个名字会更加烦人。幸运的是,有一种方便的简写语法!

使用字段初始化简写语法

由于清单 5-4 中的参数名与结构体字段名完全相同,我们可以使用 字段初始化简写语法(field init shorthand) 来重写 build_user,使其行为完全相同,但消除了 usernameemail 的重复,如清单 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:一个使用字段初始化简写语法的 build_user 函数,因为 usernameemail 参数与结构体字段同名

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

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

通常,创建一个新的结构体实例时,会包含另一个实例的大部分值,但会改变其中一些值。你可以使用 结构体更新语法(struct update syntax) 来实现这一点。

首先,在清单 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:创建一个除了一个值外,其余值都来自 user1 的新的 User 实例

使用结构体更新语法,我们可以用更少的代码实现相同的效果,如清单 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 值不同,但 usernameactivesign_in_count 字段的值与 user1 相同。..user1 必须放在最后,以指定任何剩余字段应从 user1 中对应的字段获取值,但我们可以选择以任意顺序为任意数量的字段指定值,而不管这些字段在结构体定义中的顺序如何。

注意,结构体更新语法使用了类似于赋值的 =;这是因为它会移动数据,正如我们在 “变量与数据交互的方式:Move” 部分中看到的那样。在这个例子中,创建 user2 后,我们不能再使用 user1,因为 user1username 字段中的 String 数据被移动(move)到了 user2 中。如果我们为 user2emailusername 都提供了新的 String 值,因此只使用了 user1 中的 activesign_in_count 值,那么在创建 user2 后,user1 仍然有效。activesign_in_count 都属于实现了 Copy Trait 的类型,因此会应用我们在 “只存在于栈上的数据:Copy” 部分讨论的行为。在这个例子中,我们仍然可以使用 user1.email,因为它的值 没有 被移出(move out)。

使用没有命名字段的元组结构体创建不同类型

Rust 还支持一种看起来类似于元组的结构体,称为 元组结构体(tuple structs)。元组结构体具有结构体名称提供的额外含义,但没有与字段关联的名称;它们只包含字段的类型。当你希望为整个元组赋予一个名称并使其成为一种不同于其他元组的类型,以及当像常规结构体那样为每个字段命名会显得冗余或繁琐时,元组结构体就很有用。

要定义一个元组结构体,以 struct 关键字和结构体名称开头,后跟元组中的类型。例如,这里我们定义和使用了两个名为 ColorPoint 的元组结构体:

文件名: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);
}

请注意,blackorigin 的值是不同类型的,因为它们是不同元组结构体的实例。你定义的每个结构体都是其独立的类型,即使结构体内部的字段类型可能相同。例如,一个接受 Color 类型参数的函数不能接受 Point 作为参数,尽管这两种类型都由三个 i32 值组成。此外,元组结构体实例与元组相似,你可以将它们解构成独立的组成部分,也可以使用 . 后跟索引来访问单个值。与元组不同的是,解构元组结构体时需要指明结构体的类型。例如,我们会写 let Point(x, y, z) = point

没有任何字段的单元结构体

你还可以定义没有任何字段的结构体!这些被称为 单元结构体(unit-like structs),因为它们的行为类似于我们在 “元组类型” 部分中提到的单元类型 ()单元结构体在当你需要在某种类型上实现一个 Trait,但又不想在该类型本身存储任何数据时非常有用。我们将在第 10 章讨论 Trait。下面是一个声明和实例化名为 AlwaysEqual 的单元结构体的例子:

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

fn main() {
    let subject = AlwaysEqual;
}

要定义 AlwaysEqual,我们使用 struct 关键字,后跟我们想要的名称,然后是一个分号。不需要大括号或圆括号!然后,我们可以用类似的方式在 subject 变量中获取 AlwaysEqual 的实例:使用我们定义的名称,不带任何大括号或圆括号。想象一下,稍后我们将为这个类型实现某种行为,使得 AlwaysEqual 的每个实例总是等于任何其他类型的每个实例,这或许是为了测试目的而有一个已知结果。我们不需要任何数据来实现这种行为!你将在第 10 章看到如何定义 Trait 并将其实现到任何类型上,包括单元结构体。

结构体数据的所有权

在清单 5-1 的 User 结构体定义中,我们使用了拥有的 String 类型,而不是 &str 字符串切片类型。这是一个刻意的选择,因为我们希望这个结构体的每个实例都拥有它的所有数据,并且这些数据只要整个结构体有效就一直有效。

结构体也可以存储对由其他地方拥有的数据的引用,但这需要使用 生命周期(lifetimes),这是我们将在第 10 章讨论的 Rust 特性。生命周期确保结构体引用的数据在结构体本身有效期间始终有效。假设你尝试在结构体中存储引用而不指定生命周期,如下所示;这是行不通的:

文件名: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)来解决这类错误。