0%

Rust 权威指南(04):使用结构体来组织相关联的数据

结构体是一种自定义数据类型,允许我们命名多个相关的值并将它们组织成一个有机的结合体。结构体是和枚举类型是用来创建新类型的基本工具。

定义并实例化结构体

结构体和元组有些类似:

  • 和元组一样,结构体中的数据可以拥有不同的类型
  • 和元组不一样的是,结构体需要给每个数据赋予名字以清楚地表明它的含义。也正是因为这些名字,结构体的使用比元组更加灵活:不需要依赖顺序索引来指定或访问实例中的值

使用 struct 关键字来定义并命名结构体,在随后的花括号中声明所有数据的名字及类型,这些数据也被称为字段。为了使用定义好的结构体,需要为每个字段赋予具体的值来创建结构体实例。可以通过声明结构体名称,并使用一对花括号包含键值对来创建实例,赋值顺序并不需要严格对应在结构体中声明它们的顺序。

获得结构体实例后,通过 . 来访问实例中的特定字段。如果结构体实例是可变的,还可以通过点号来修改字段的值。一旦实例可变,那么实例中的所有字段都是可变的,Rust 不允许单独声明某一部分字段的可变性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}

fn build_user(email: String, username: String) -> User {
User {
email: email,
username: username,
active: true,
sign_in_count: 1,
}
}

fn main() {
let mut user1 = build_user(
String::from("test@163.com"),
String::from("test")
);

println!("sign_in_count {}", user1.sign_in_count);

user1.sign_in_count = 2;
println!("sign_in_count {}", user1.sign_in_count);
}

当变量名与字段名相同时,可以使用一种名为 字段初始化简写 的语法。因此 build_user() 可以简化为:

1
2
3
4
5
6
7
8
fn build_user(email: String, username: String) -> User {
User {
email,
username,
active: true,
sign_in_count: 1,
}
}

当根据某个实例创建其他实例时,很多情形下,在新创建的实例中,除了需要修改小部分字段,其它字段与旧实例完全相同。可以使用结构体更新语法来快速实现此类新实例的创建。

1
2
3
4
5
let user2 = User {
email: String::from("test2@163.com"),
username: String::from("test2"),
..user1
};

这里的 双点号.. 表明剩下的那些还未显示赋值的字段都与给定实例拥有相同的值。

除了以上方法,还可以使用一种类似于元组的方式来定义结构体,这种结构体也被称为元组结构体。元组结构体同样拥有表明自身含义的名称,但是无需在声明它时对其字段进行命名,仅保留字段类型即可。一般来说,当你想要给元组赋予名字,并使其区别于其他拥有同样定义的元组时,就可以使用元组结构体。

定义元组结构体使用 struct 关键字开头,并由结构体名称及元组中的类型定义组成。元组结构体实例的行为就像元组一样:可以通过模式匹配将它们解构为单独的部分,也可以通过 . 及索引来访问特定字段。

1
2
3
4
5
6
7
fn main() {
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
}

注意这里 black 和 origin 是不同的类型,因为它们两个分别是不同元组结构体的实例。

Rust 也允许我们创建没有任何字段的结构体,它们也被称为空结构体。当你需要在某些类型上实现一个 trait,却不需要在该类型中存储任何数据时,空结构体就能发挥相应的作用。

1
2
3
4
5
struct AlwaysEqual;

fn main() {
let subject = AlwaysEqual;
}

一个使用结构体的实例程序

如下是一个使用结构体的实例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};

println!("area is {}", area(&rect1));
}

fn area(rect: &Rectangle) -> u32 {
rect.width * rect.height
}

如果我们在调试该程序的时候,想打印出 Rectangle 实例:

1
println!("rect is {}", rect1);

但是编译该代码,会出现如下编译错误

1
error[E0277]: `Rectangle` doesn't implement `std::fmt::Display`

println! 宏里的格式化文本中的花括号会告知 println!() 使用名为 Display 的格式化方法。所有基础类型都默认实现了 Display,但 Rust 默认没有为结构体提供 Display 实现。为了解决该问题,可以使用如下方法:

  • 把标识符 :? 或者 :#?(输出形式更加可读) 放入格式化文本的花括号中,它会告知 println! 当前结构体需要使用名为 Debug 的格式化输出。Debug 是另外一种格式化 trait,它可以让我们在调试代码的时候以一种对开发者友好的形式打印出结构体
  • 需要为自己的结构体添加 #[derive(Debug)] 注解,显式地为结构体选择打印调试信息的功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};

println!("rect is {:?}", rect1);
println!("area is {}", area(&rect1));
}

fn area(rect: &Rectangle) -> u32 {
rect.width * rect.height
}

方法

方法与函数类似,都使用 fn 关键字及一个名称来进行声明,都可以拥有参数和返回值,也都包含了一段在调用时执行的代码。但是,方法与函数依然是两个概念,因为方法总是被定义在某个结构体(或者枚举类型、trait 对象)的上下文中,并且它们的第一个参数永远都是 self,用于指代调用该方法的结构体实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};

println!("rect is {:?}", rect1);
println!("area is {}", rect1.area());
}

impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
  • 为了在 Rectangle 上下文环境中定义该函数,需要将 area 函数移动到一个由 impl(implementation)关键字起始的代码块中,并将函数第一个参数名为为 self
  • area 调用的地方全部改写为方法调用形式,方法调用是通过在实例后面加点号,并直接跟上方法名、括号、及可能的参数来实现

由于方法的声明过程是放在 impl Rectangle 块中,Rust 能够将 self 类型推断为 Recentagle。但是仍然需要在 self 前添加 & 以指明采用不可变借用形式,如果不指定 &,则会获取所有权,如果使用 &mut self 则采用可变借用。

当使用 object.method() 调用方法时,Rust 会自动为 object 添加 &&mut*,以使得其能够符合方法的签名。

使用方法有助于代码的组织,可以将某个类型的实例需要的功能放置在同一个 impl 块中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};

let rect2 = Rectangle {
width: 10,
height: 10,
};

println!("rect is {:?}", rect1);
println!("area is {}", rect1.area());
println!("rect1 {:?} can hold rect2 {:?}: {}", rect1, rect2, rect1.can_hold(&rect2))
}

impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}

fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}

除了方法,impl 块还允许我们定义不用接收 self 作为参数的函数。由于该类函数与结构体相互关联,因此也被称为关联函数(associated function)。之所以称其为函数而不是方法,因为他们不会作用于某个具体的结构体实例。我们可以在类型名称后添加 :: 来调用关联函数。关联函数位于结构体的命名空间中,:: 语法不仅用于调用关联函数,还被用于模块创建的命名空间。String::from 就是关联函数的一种。

关联函数常被用作构造器来返回一个结构体的新实例。

1
2
3
4
5
6
7
8
9
10
11
12
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}

fn main{
let rect3 = Rectangle::square(10);
}

每个结构体可以拥有多个 impl 块:

1
2
3
4
5
6
7
8
9
10
11
impl Rectangle {
fn method1(&self) {

}
}

impl Rectangle {
fn method2(&self) {

}
}