结构,或者说结构体,是一种自定义数据类型,它允许我们命名多个相关的值并将它们组成一个有机的结合体。
假如你曾经有过面向对象的编程经历,那么你可以把结构体视作对象中的数据属性。在本篇文章中,我们会首先对比元组与结构体之间的异同,并演示如何使用结构体。
然后我们再讲一下如何定义方法和关联函数,它们可以指定那些与结构体数据相关的行为。结构体与枚举类型(后面学习)是用来创建新类型的基本工具,这些特定领域中的新类型同样可以享受到Rust编译时类型检查系统的所有优势。
结构体与我们在前面文章中讨论过的元组有些相似。和元组一样,结构体中的数据可以拥有不同的类型。
而和元组不一样的是,结构体需要给每个数据赋予名字以便清楚地表明它们的意义。正是因为有了这些名字,结构体的使用要比元组更加灵活:你不再需要依赖顺序索引来指定或访问实例中的值。
关键字 struct 被用来定义并命名结构体,一个良好的结构体名称应当能够反映出自身数据组合的意义。除此之外,我们还需要在随后的花括号中声明所有数据的名字及类型,这些数据也被称作字段。示例5-1中展示了一个用于存储账户信息的结构体定义:
// 示例5-1:User结构体的定义
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
为了使用定义好的结构体,我们需要为每个字段赋予具体的值来创建结构体实例。可以通过声明结构体名称,并使用一对花括号包含键值对来创建实例。
其中的键对应字段的名字,而值则对应我们想要在这些字段中存储的数据。这里的赋值顺序并不需要严格对应我们在结构体中声明它们的顺序。
换句话说,结构体的定义就像类型的通用模板一样,当我们将具体的数据填入模板时就创建出了新的实例。例如,我们可以像示例5-2这样来声明一个特定的用户。
// 示例5-2:创建一个User结构体的实例
let user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
在获得了结构体实例后,我们可以通过点号来访问实例中的特定字段。如果你想获得某个用户的电子邮件地址,那么可以使用 user1.email 来获取。另外,假如这个结构体的实例是可变的,那么我们还可以通过点号来修改字段中的值。示例5-3展示了如何修改一个可变 User 实例中 email 字段的值。
// 示例5-3:修改User实例中email字段的值
let mut user1 = User {
email: String::from("[email protected]"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
user1.email = String::from("[email protected]");
需要注意的是,一旦实例可变,那么实例中的所有字段都将是可变的。Rust不允许单独声明某一部分字段的可变性。如同其他表达式一样,我们可以在函数体的最后一个表达式中构造结构体实例,来隐式地将这个实例作为结果返回。
示例5-4中的 build_user 函数会使用传入的邮箱和用户名参数构造并返回 User 实例。另外两个字段 active 和 sign_in_count 则分别被赋予了值 true 和 1。
// 示例5-4:一个接收邮箱和用户名作为参数并返回User实例的函数build_user
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}
在函数中使用与结构体字段名相同的参数名可以让代码更加易于阅读,但分别两次书写 email 和 username 作为字段名与变量名则显得有些烦琐了,特别是当结构体拥有较多字段时。Rust为此提供了一个简便的写法。
由于示例5-4中的参数与结构体字段拥有完全一致的名称,所以我们可以使用名为字段初始化简写(field init shorthand)的语法来重构 build_user 函数。
这种语法不会改变函数的行为,但却能让我们免于在代码中重复书写 email 和 username,如示例5-5所示。
/* 示例5-5:build_user函数中使用了相同的参数名与字段名,
并采用了字段初始化简写语法进行编写 */
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}
上面的代码首先创建了一个拥有 email 字段的 User 结构体实例。我们希望使用 build_user 函数的 email 参数来初始化这个实例的 email 字段。
由于字段 email 与参数 email 拥有相同的名字,所以我们不用书写完整的 email: email 语句,只保留 email 即可。
在许多情形下,在新创建的实例中,除了需要修改的小部分字段,其余字段的值与旧实例中的完全相同。
我们可以使用结构体更新语法来快速实现此类新实例的创建。首先,示例5-6展示了如何在不使用更新语法的前提下来创建新的 User 实例 user2。除了 email 和 username 这两个字段,其余的值都与在示例5-2中创建的 user1 实例中的值一样。
// 示例5-6:使用user1中的某些值来创建一个新的User实例
let user2 = User {
email: String::from("[email protected]"),
username: String::from("anotherusername567"),
active: user1.active,
sign_in_count: user1.sign_in_count,
};
通过结构体更新语法,我们可以使用更少的代码来实现完全相同的效果,如示例5-7所示。这里的双点号 .. 表明剩下的那些还未被显式赋值的字段都与给定实例拥有相同的值。
/* 示例5-7:使用结构体更新语法来为一个User实例设置新的email和username字段的值,
并从user1实例中获取剩余字段的值 */
let user2 = User {
email: String::from("[email protected]"),
username: String::from("anotherusername567"),
..user1
};
示例5-7中的代码新创建了一个实例 user2,它的 email 和 username 字段的值与实例 user1 中的不同,但是 active 和 sign_in_count 字段的值与 user1 中的相同。
除了上面的方法,你还可以使用另外一种类似于元组的方式定义结构体,这种结构体也被称作元组结构体。
元组结构体同样拥有用于表明自身含义的名称,但你无须在声明它时对其字段进行命名,仅保留字段的类型即可。
一般来说,当你想要给元组赋予名字,并使其区别于其他拥有同样定义的元组时,你就可以使用元组结构体。
在这种情况下,像常规结构体那样为每个字段命名反而显得有些烦琐和形式化了。定义元组结构体时依然使用 struct 关键字开头,并由结构体名称及元组中的类型定义组成。
下面的代码中展示了两个分别叫作 Color 和 Point 的元组结构体定义:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
注意,这里的 black 和 origin 是不同的类型,因为它们两个分别是不同元组结构体的实例。你所定义的每一个结构体都拥有自己的类型,即便结构体中的字段拥有完全相同的类型。
例如,一个以 Color 类型作为参数的函数不能合法地接收 Point 类型的参数,即使它们都是由 3 个 i32 值组成的。
除此之外,元组结构体实例的行为就像元组一样:你可以通过模式匹配将它们解构为单独的部分,你也可以通过.及索引来访问特定字段。
也许会出乎你的意料,Rust允许我们创建没有任何字段的结构体!因为这种结构体与空元组()十分相似,所以它们也被称为空结构体。
当你想要在某些类型上实现一个 trait,却不需要在该类型中存储任何数据时,空结构体就可以发挥相应的作用。我们会在后面文章中讨论 trait。
在示例5-1的 User 结构体定义中,我们使用了自持所有权的 String 类型而不是 &str 字符串切片类型。这是一个有意为之的选择,因为我们希望这个结构体的实例拥有自身全部数据的所有权。
在这种情形下,只要结构体是有效的,那么它携带的全部数据也就是有效的。当然,我们也可以在结构体中存储指向其他数据的引用,不过这需要用到Rust中独有的生命周期功能,这个后面会讨论。
生命周期保证了结构体实例中引用数据的有效期不短于实例本身。你也许会尝试在没有生命周期的情形下,直接在结构体中声明引用字段:
struct User {
username: &str,
email: &str,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: "[email protected]",
username: "someusername123",
active: true,
sign_in_count: 1,
};
}
但这段代码可没办法通过检查,Rust会在编译过程中报错,提示我们应该指定生命周期:
errorE0106: missing lifetime specifier
-->
|
2 | username: &str,
| ^ expected lifetime parameter
errorE0106: missing lifetime specifier
-->
|
3 | email: &str,
| ^ expected lifetime parameter
不用着急,我们会在后面学习如何解决上面这些错误,并合法地在结构体中存储引用字段。现在,我们先使用持有自身所有权的 String 而不是像 &str 一样的引用来解决这个问题。