自定义数据类型
结构体
定义结构体,需要使用 struct
关键字并为整个结构体提供一个名字。结构体的名字需要描述它所组合的数据的意义。接着,在大括号中,定义每一部分数据的名字和类型,我们称为 字段(field)。
示例展示了一个存储用户账号信息的结构体:
rust
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
创建结构体实例
一旦定义了结构体后,为了使用它,通过为每个字段指定具体值来创建这个结构体的 实例。创建一个实例需要以结构体的名字开头,接着在大括号中使用 key: value
键-值对的形式提供字段,其中 key 是字段的名字,value 是需要存储在字段中的数据值。实例中字段的顺序不需要和它们在结构体中声明的顺序一致。
创建实例示例:
rust
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
let user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
}
为了从结构体中获取某个特定的值,可以使用点号。举个例子,想要用户的邮箱地址,可以用 user1.email
。如果结构体的实例是可变的,可以使用点号并为对应的字段赋值。
示例:
rust
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}
fn main() {
// 注意:实例是可变的
let mut user1 = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
// 获得邮箱地址
let _em = user1.email;
// 修改用户的邮箱
user1.email = String::from("anotheremail@example.com");
}
函数返回结构体
注意整个实例必须是可变的;Rust 并不允许只将某个字段标记为可变。另外需要注意同其他任何表达式一样,我们可以在函数体的最后一个表达式中构造一个结构体的新实例,来隐式地返回这个实例。
rust
fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}
实例结构体的其他方式
使用字段初始化简写
我们可以使用 字段初始化简写语法来重写 build_user
,这样其行为与之前完全相同,不过无需重复 email
和 username
了,示例:
rust
fn build_user(email: String, username: String) -> User {
User {
email, // 无需重复写参数名
username, // 无需重复写参数名
active: true,
sign_in_count: 1,
}
}
因为 email
字段与 email
参数有着相同的名称,则只需编写 email
而不是 email: email
,username
同理。
从其他实例创建实例
使用旧实例的大部分值但改变其部分值来创建一个新的结构体实例通常是很有用的。这可以通过 结构体更新语法实现。
rust
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"),
sign_in_count: user1.sign_in_count,
..user1
};
}
元组结构体
元组结构体有着结构体名称提供的含义,但没有具体的字段名,只有字段的类型。要定义元组结构体,以 struct
关键字和结构体名开头并后跟元组中的类型。例如,下面是两个分别叫做 Color
和 Point
元组结构体的定义和用法:
rust
struct Color(i32, i32, i32);
struct Point(i32, i32);
fn main() {
let black = Color(0, 0, 0);
let origin = Point(0, 0);
}
注意 black
和 origin
值的类型不同,因为它们是不同的元组结构体的实例。你定义的每一个结构体有其自己的类型,即使结构体中的字段可能有着相同的类型。
元组可以进行匹配和嵌套解构。如下是一个复杂结构体的例子,其中结构体和元组嵌套在元组中,并将所有的原始类型解构出来:
rust
fn main() {
struct Point {
x: i32,
y: i32,
}
let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 });
}
这将复杂的类型分解成部分组件以便可以单独使用我们感兴趣的值。
通过模式解构是一个方便利用部分值片段的手段,比如结构体中每个单独字段的值。
有时可以忽略模式结构出来的一些值。使用 _ 模式可以忽略模式中全部或部分值,或者使用 .. 忽略所剩部分的值。例如:
rust
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, ..) => {
println!("Some numbers: {}, {}", first, third)
}
}
}
除了以上采用解构的方式外还有可以采用索引的方式tuple.index
获得目标值。例如:
rust
fn main() {
let numbers = ("hello", "world");
let second = numbers.1; // 索引的方式
assert_eq!("world", second,
"This is not the 2nd number in the tuple!");
}
单元结构体
单元结构体是没有任何字段的结构体,它的定义完全不包含字段(fields)列表。如果你定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用 单元结构体:这样的结构体隐式定义了其类型的同名常量(值)。例如:
rust
#![allow(unused)]
fn main() {
struct AlwaysEqual;
let subject = AlwaysEqual;
// 我们不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为单元结构体,然后再为它实现某个trait
// 有关trait的知识我们在后面会介绍。
impl SomeTrait for AlwaysEqual {
}
}
枚举
枚举与结构体相似,但又有所不同。结构体给予你将字段和数据聚合在一起的方法,像 Rectangle
结构体有 width
和 height
两个字段。而枚举给予你将一个值成为一个集合之一的方法。所以,结构体是同一个概念不同属性的组合,而枚举则是同一个概念下的不同选项。
要声明一个枚举,请写enum
,并使用一个包含选项的代码块,用逗号分隔。就像 struct
一样,最后一部分可以有逗号,也可以没有。示例:
rust
enum IpAddrKind {
V4,
V6,
}
枚举类型是一个类型,它会包含所有可能的枚举成员, 而枚举值是该类型中的具体某个成员的实例。
创建枚举成员的实例:
rust
let four = IpAddrKind::V4;
let six = IpAddrKind::V6;
注意枚举的成员位于其标识符的命名空间中,并使用两个冒号分开。这么设计的益处是现在 IpAddrKind::V4
和 IpAddrKind::V6
都是 IpAddrKind
类型的。
用枚举替代结构体还有另一个优势:每个成员可以处理不同类型和数量的数据。IPv4 版本的 IP 地址总是含有四个值在 0 和 255 之间的数字部分。如果我们想要将 V4
地址存储为四个 u8
值而 V6
地址仍然表现为一个 String
,这就不能使用结构体了。枚举则可以轻易的处理这个情况:
rust
enum IpAddr {
V4(u8, u8, u8, u8),
V6(String),
}
let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));
每个枚举实例都有一个判别值/判别式(discriminant),它是一个与此枚举实例关联的整数,用来确定它持有哪个变体。可以通过 mem::discriminant 函数来获得对这个判别值的不透明引用。
如果枚举的任何变体都没有附加字段,则可以直接设置和访问判别值。
可以使用操作符 as 通过数值转换将这些枚举类型转换为整型。枚举可以可选地指定每个判别值的具体值,方法是在变体名后面追加 = 和常量表达式。如果声明中的第一个变体未指定,则将其判别值设置为零。对于其他未指定的判别值,它比照前一个变体的判别值按 1 递增。例如下面的示例:
rust
#![allow(unused)]
fn main() {
enum Foo {
Bar, // 0
Baz = 123, // 123
Quux, // 124 自增1
}
let baz_discriminant = Foo::Baz as u32;
assert_eq!(baz_discriminant, 123);
}
此外,同一枚举中,两个变体使用相同的判别值是错误的。
rust
#![allow(unused)]
fn main() {
enum SharedDiscriminantError {
SharedA = 1,
SharedB = 1
}
enum SharedDiscriminantError2 {
Zero, // 0
One, // 1
OneToo = 1 // 1 (和前值冲突!)
}
}
当前一个变体的判别值是当前表形允许的的最大值时,再使用默认判别值就也是错误的。
rust
#![allow(unused)]
fn main() {
#[repr(u8)]
enum OverflowingDiscriminantError {
Max = 255,
MaxPlusOne // 应该是256,但枚举溢出了
}
#[repr(u8)]
enum OverflowingDiscriminantError2 {
MaxMinusOne = 254, // 254
Max, // 255
MaxPlusOne // 应该是256,但枚举溢出了。
}
}
在枚举中,可以将任意类型的数据放入枚举成员中:例如字符串、数字类型或者结构体。甚至可以包含另一个枚举。示例:
rust
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
这个枚举有四个含有不同类型的成员:
Quit
没有关联任何数据。Move
类似结构体包含命名字段。Write
包含单独一个String
。ChangeColor
包含三个i32
。
注:此知识点可能对于刚开始学习Rust较难,可以在学习完初级知识点后返回来了解本知识点。 Rust 还有一些属性用于更改类型的使用方式。例如可以使用 non_exhaustive 属性指示结构体、枚举、枚举变体将来可能会添加更多字段或变体。
rust
#[non_exhaustive]
pub struct Config {
pub window_width: u16,
pub window_height: u16,
}
#[non_exhaustive]
pub enum Error {
Message(String),
Other,
}
pub enum Message {
#[non_exhaustive] Send { from: u32, to: u32, contents: String },
#[non_exhaustive] Reaction(u32),
#[non_exhaustive] Quit,
}
// 非详尽结构体能够被正常构造
let config = Config { window_width: 640, window_height: 480 };
// 非详尽结构体能够在定义的crate中完全匹配
if let Config { window_width, window_height } = config {
// ...
}
let error = Error::Other;
let message = Message::Reaction(3);
// 非详尽枚举能够在定义的crate中穷举匹配
match error {
Error::Message(ref s) => { },
Error::Other => { },
}
match message {
Message::Send { from, to, contents } => { },
Message::Reaction(id) => { },
Message::Quit => { },
}
在定义crate内,non_exhaustive 没有效果。在定义crate之外,带有注释的类型 non_exhaustive 具有在添加新字段或变体时保持向后兼容性的限制。
非详尽类型不能在定义crate之外构造:
- 无法使用StructExpression(包括结构体更新语法)构造非详尽的变体(struct或enum变体) 。
- enum可以构造实例。
rust
// `Config`, `Error`, and `Message` are types defined in an upstream crate that have been
// annotated as `#[non_exhaustive]`.
use upstream::{Config, Error, Message};
// Cannot construct an instance of `Config`, if new fields were added in
// a new version of `upstream` then this would fail to compile, so it is
// disallowed.
let config = Config { window_width: 640, window_height: 480 };
// Can construct an instance of `Error`, new variants being introduced would
// not result in this failing to compile.
let error = Error::Message("foo".to_string());
// Cannot construct an instance of `Message::Send` or `Message::Reaction`,
// if new fields were added in a new version of `upstream` then this would
// fail to compile, so it is disallowed.
let message = Message::Send { from: 0, to: 1, contents: "foo".to_string(), };
let message = Message::Reaction(0);
// Cannot construct an instance of `Message::Quit`, if this were converted to
// a tuple-variant `upstream` then this would fail to compile.
let message = Message::Quit;
在定义crate之外匹配非详尽类型时存在限制:
- 当模式匹配一个非穷尽变体时,必须使用StructPattern句法进行匹配,其匹配臂必须有一个为
..
。元组变体的构造函数的可见性降低为min($vis, pub(crate))
。 - 当模式匹配在一个非穷尽的枚举(enum)上时,增加对单个变体的匹配无助于匹配臂需满足枚举变体的穷尽性(exhaustiveness)的这一要求。