定义与实例化结构体
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
字段会得到值 true
,sign_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
函数
将函数参数命名为与结构字段相同的名字,是有意义的,但必须重复 email
和 username
两个字段名字和变量,就有点乏味了。如果结构体有更多字段,那么重复每个字段的名字,就会更加烦人。幸运的是,有种方便的简写!
使用字段初始化简写法
Using the Field Init Shorthand
由于清单 5-4 中的参数名字,和结构体字段名字完全相同,我们可以使用 字段初始化简写法,field init shorthand 语法,重写 build_user
,使其行为完全相同,而不重复使用 username
和 email
,如下清单 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:由于 username
与 email
两个参数有着与结构体字段同样的名字,而使用了字段初始化简写法的 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
值,却有着来自 user1
的 username
、active
和 sign_in_count
等字段同样值的实例。..user1
必须放在最后,以指定其余字段应从 user1
中的相应字段获取值,但我们可以选择以任意顺序,为任意多个字段指定值,与结构体定义中的字段顺序无关。
请注意,结构更新语法像赋值一样,用到了 =
操作符;这是因为他迁移了数据,正如我们在 变量和数据相互作用:迁移 小节中所看到的。在本例中,创建出 user2
后,我们不能再将 user1
作为一个整体使用,因为 user1
的 username
字段中的那个 String
,已被迁移到了 user2
中。如果我们同时为 user2
的 email
和 username
,赋予了新的 String
值,从而只使用 user1
中的 active
和 sign_in_count
两个值,那么user1
在创建出 user2
后,仍然会有效。active
和 sign_in_count
都是实现了 Copy
特质的类型,因此将适用我们在 唯栈数据:拷贝 小节中,曾讨论过的行为。
使用没有命名字段的元组结构体,创建不同类型
Using Tuple Structs without Named Fields to Create Different Types
Rust 还支持称为 元组结构体,tuple structs 的,看起来类似于元组的结构体。元组结构体有着结构体名称所提供的附带意义,但却没有与其字段相关联的名字;相反,他们只有字段的类型。如果咱们打算给这整个元组一个名字,并将该元组构造为不同于其他元组的类型,并且在给常规结构体中的各个字段命名会显得冗长或多余时,那么元组结构体就非常有用。
要定义元组结构体,就要以 struct
关键字以及该结构体的名字开头,然后是元组中的类型。例如,下面我们定义并使用了两个名为 Color
和 Point
的元组结构体:
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); }
请注意,其中的 black
和 origin
两个值,属于不同类型,因为他们是不同元组结构体的实例。咱们定义的每个结构体,都属于其自己的类型,即使结构体中的字段,可能具有相同类型。例如,取 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
),来修复这类报错。