定义与实例化结构体

Defining and Instantiating Structs

结构体与 元组类型 小节中,讨论过的元组类似,他们都保存了多个相关的值。与元组一样,结构体中的数据也可以是不同的类型。与元组不同的是,在结构体中,我们会为每条数据命名,这样就能清楚地知道,这些值的含义。添加这些名字,意味着结构体比元组更灵活:咱们不必依赖数据的顺序,来指定或访问某个实例的值。

要定义某个结构体,我们要输入关键字 struct 并为这整个结构体命名。结构体的名称,应描述被组合在一起的这些数据的意义。然后,在花括号内,咱们要定义出我们称之为 字段,fields 的,这些数据的名字和类型。例如,下面清单 5-1 给出了一个,存储了一名用户账户信息的结构体。

#![allow(unused)]
fn main() {
struct User {
    active: bool,
    username: String,
    email: String,
    sign_in_count: u64
}
}

清单 5-1:User 结构体的定义

要在咱们定义了某个结构体后使用他,我们就要通过为每个字段,指定出具体的值,创建出该结构体的一个 实例,instance。我们通过指明该结构体的名字,然后添加一对,包含着一些其中键是字段的名字,值为我们要存储在这些字段中数据的 key: value 键值对的花括号。我们不必按照在结构体中声明字段的同样顺序,指定这些字段。换句话说,结构体的定义,就像是该类型的通用模板,而实例则将以特定数据,填充该模板,以创建出该类型的值来。例如,我们可以如下清单 5-2 所示,声明出一个特定的用户。

文件名:src/main.rs

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

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,给出了一个以给定电子邮件和用户名,返回一个 User 实例的 build_user 函数。active 字段会得到值 truesign_in_count 会得到值 1

#![allow(unused)]
fn main() {
fn build_user(email: String, username: String) -> User {
    User {
        email: email,
        username: username,
        active: true,
        sign_in_count: 1,
    }
}
}

清单 5-4:取一个电子邮件和用户名,并返回一个 User 实例的 build_user 函数

将函数参数命名为与结构字段相同的名字,是有意义的,但必须重复 emailusername 两个字段名字和变量,就有点乏味了。如果结构体有更多字段,那么重复每个字段的名字,就会更加烦人。幸运的是,有种方便的简写!

使用字段初始化简写法

Using the Field Init Shorthand

由于清单 5-4 中的参数名字,和结构体字段名字完全相同,我们可以使用 字段初始化简写法,field init shorthand 语法,重写 build_user,使其行为完全相同,而不重复使用 usernameemail,如下清单 5-5 所示。

#![allow(unused)]
fn main() {
fn build_user(email: String, username: String) -> User {
    User {
        email,
        username,
        active: true,
        sign_in_count: 1,
    }
}
}

清单 5-5:由于 usernameemail 两个参数有着与结构体字段同样的名字,而使用了字段初始化简写法的 build_user 函数

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

使用结构体更新语法,从另一实例创建出实例

Creating Instances from Other Instances with Struct Update Syntax

创建一个其中包含另一实例中的大部分值,但要更改其中某些值的新实例,通常很有用。咱们可以使用 结构体更新语法,完成这一点。

首先,在下面清单 5-6 中,我们给出了在不使用这种更新语法下,如何常规地在 user2 中创建一个新 User 实例。我们为 email 字段设置了一个新值,但对其他字段,使用了咱们在清单 5-2 中,创建的 user1 中的那些同样值。

fn main() {
    // --跳过代码--

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

清单 5-6:创建一个用到 user1 中值的新 User 实例

如下清单 5-7 所示,使用结构体更新语法,我们可以用较少的代码,实现相同的效果。语法 .. 指明了,其余未显式设置的字段,应有着与给定实例中字段,相同的值。

fn main() {
    // --跳过代码--

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

清单 5-7:使用结构体更新语法设置某个 User 实例的新 email 值,但使用来自 user1 的其余值

清单 5-7 中的代码,也创建出了一个 user2 中,有着不同 email 值,却有着来自 user1usernameactivesign_in_count 等字段同样值的实例。..user1 必须放在最后,以指定其余字段应从 user1 中的相应字段获取值,但我们可以选择以任意顺序,为任意多个字段指定值,与结构体定义中的字段顺序无关。

请注意,结构更新语法像赋值一样,用到了 = 操作符;这是因为他迁移了数据,正如我们在 变量和数据相互作用:迁移 小节中所看到的。在本例中,创建出 user2 后,我们不能再将 user1 作为一个整体使用,因为 user1username 字段中的那个 String,已被迁移到了 user2 中。如果我们同时为 user2emailusername,赋予了新的 String 值,从而只使用 user1 中的 activesign_in_count 两个值,那么user1 在创建出 user2 后,仍然会有效。activesign_in_count 都是实现了 Copy 特质的类型,因此将适用我们在 唯栈数据:拷贝 小节中,曾讨论过的行为。

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

Using Tuple Structs without Named Fields to Create Different Types

Rust 还支持称为 元组结构体,tuple structs 的,看起来类似于元组的结构体。元组结构体有着结构体名称所提供的附带意义,但却没有与其字段相关联的名字;相反,他们只有字段的类型。如果咱们打算给这整个元组一个名字,并将该元组构造为不同于其他元组的类型,并且在给常规结构体中的各个字段命名会显得冗长或多余时,那么元组结构体就非常有用。

要定义元组结构体,就要以 struct 关键字以及该结构体的名字开头,然后是元组中的类型。例如,下面我们定义并使用了两个名为 ColorPoint 的元组结构体:

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

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

请注意,其中的 blackorigin 两个值,属于不同类型,因为他们是不同元组结构体的实例。咱们定义的每个结构体,都属于其自己的类型,即使结构体中的字段,可能具有相同类型。例如,取 Color 类型参数的某个函数,就不能取某个 Point 作为参数,尽管这两种类型,都是由三个 i32 值组成。此外,元组结构体的实例,在咱们可以将其解构为他们的单独部分,以及咱们可以使用 . 后跟索引来访问单独值两个方面,类似于元组。

不带任何字段的类单元值结构体

Unit-Like Structs Without Any Fields

咱们还可以定义出,没有任何字段的结构体!这些结构体被称为 类单元值结构体,unit-like structs,因为他们的行为类似于我们在 元组类型 小节中,提到的单元值类型 ()。当咱们需要在某种类型上,实现某个特质,但又没有咱们打算存储在该类型中的任何数据时,类单元值结构体就会派上用场。我们将在第 10 章讨论特质。下面是个名为 AlwaysEqual 的单元值结构体的声明和实例化示例:

struct AlwaysEqual;

fn main() {
    let subject = AlwaysEqual;
}

要定义出 AlwaysEqual,我们需要使用 struct 关键字、我们打算使用的名字,及随后的分号。无需使用花括号或圆括号!然后,我们就能以类似的方法,获取到 subject 变量的一个 AlwaysEqual 的实例:使用我们定义的名字,不需要任何花括号或圆括号。请设想,以后我们将为这种类型实现的行为:AlwaysEqual 的每个实例,总是等于任何其他类型的每个实例,或许是为测试目的,有个已知结果。我们不需要任何数据,来实现这种行为!咱们将在第 10 章中,看到如何定义特质,并在任意类型上实现他们,包括类单元值结构体。

结构体数据的所有权

在清单 5-1 中的 User 结构体定义中,我们使用了自由的 String 类型,而不是 &str 的字符串切片类型。这是有意为之,因为我们希望该结构体的各个实例,都拥有其所有数据,同时只要整个结构有效,这些数据就有效。

结构体存储指向其他数据的引用,也是可行的,但这样做要用到我们将在第 10 章讨论的 Rust 生命周期,lifetimes 特性。生命周期会确保结构体所引用的数据,在结构体存在期间一直有效。假设咱们试图在结构体中,存储某个引用而未指定生命周期,就像下面这样;这是行不通的:

文件名:src/main.rs

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

fn main() {
    let user1 = User {
        email: "someone@example.com",
        username: "someusername123",
        active: true,
        sign_in_count: 1,
    };
}

编译器会抱怨他需要生命周期说明符,lifetime specifiers:

$ cargo run
   Compiling structs_demo v0.1.0 (/home/peng/rust-lang/projects/structs_demo)
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_demo` due to 2 previous errors

在第 10 章中,我们将讨论如何修复这些报错,以便咱们在结构体中存储引用,但现在,我们将使用自有类型(如 String)而不是引用(如 &str),来修复这类报错。

Last change: 2023-12-14, commit: a763af3