结构体(Struct)

本文大部分内容翻译自:The Rust Programming Language

1. Defining and Instantiating Structs

  • struct中可以包含不同类型元素(跟tuple一样)
  • 每个元素有自己的名字(与tuple不同)- 所以struct在访问元素时比tuple灵活

1.1. 定义结构体

1
2
3
4
5
6
struct User {
active: bool,
username: String,
email: String,
sign_in_count: u64,
}

里面的元素叫做“fields”。

1.2. 使用结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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,
};
}

.获得结构体中的“field”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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,
};

user1.email = String::from("anotheremail@example.com");
}

整个结构体实例必须是可变的,不允许只让一部分“fields”是可变的。

1.3. 字段初始化简写语法

当用于初始化的变量名与“fields”的名字相同时,可以省略“fields”的名字:

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

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

fn main() {
let user1 = build_user(
String::from("someone@example.com"),
String::from("someusername123"),
);
}

1.4. 用另一个实例来初始化新实例

使用user1初始化user2
麻烦的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 {
active: user1.active,
username: user1.username,
email: String::from("another@example.com"),
sign_in_count: user1.sign_in_count,
};
}

使用结构体的update语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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"),
..user1
};
}

..表示除了明确指出的fields之外,其它fields跟user1都相同。user1必须最后出现。

这样创建user2之后,不能再使用user1了,因为user1中的username是一个String类型,执行的是移动(move)操作,即usernamemoved到了user2

但如果将emailusername都赋予新String值,只使用user1activesign_in_count,那么还可以继续使用user1,因为activesign_in_count是布尔值和整数,使用的是Copy而不是move

1.5. 元组结构体(Tuple Structs)

1
2
3
4
5
6
7
struct Color(i32, i32, i32);  // tuple struct named Color
struct Point(i32, i32, i32); // tuple struct named Point

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

Rust还支持看上去类似于元组的结构体,称为元组结构体(Tuple Structs)。元组结构体具有结构体名称提供的附加含义,但没有与其字段关联的名称,它们只有字段的类型。当你想给整个元组命名并使该元组与其他元组类型不同,而且给每个字段命名会显得冗余时,元组结构体非常有用。

1.6. Unit-like structs

没有任何fields的结构体——unit-like structs。

Unit-like structs can be useful when you need to implement a trait on some type but don’t have any data that you want to store in the type itself.

例:

1
2
3
4
5
struct AlwaysEqual;

fn main() {
let subject = AlwaysEqual;
}

2. An Example Program Using Structs

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

Putting the specifier :? inside the curly brackets tells println! we want to use an output format called Debug. The Debug trait enables us to print our struct in a way that is useful for developers so we can see its value while we’re debugging our code.

会有编译错误:

1
error[E0277]: `Rectangle` doesn't implement `Debug`

需要在结构体定义之前添加outer attribute #[derive(Debug)]

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

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

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

输出:

1
2
3
4
5
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle { width: 30, height: 50 }

如果使用{:#?},会输出:

1
2
3
4
5
6
7
8
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.48s
Running `target/debug/rectangles`
rect1 is Rectangle {
width: 30,
height: 50,
}

使用dbg! macro:

  • 打印到标准错误控制台流(stderr)
  • 接收一个表达式的所有权,打印出代码中调用dbg!宏时所在的文件和行号,以及该表达式的结果值,并返回该值的所有权

Here’s an example where we’re interested in the value that gets assigned to the width field, as well as the value of the whole struct in rect1:

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

fn main() {
let scale = 2;
let rect1 = Rectangle {
width: dbg!(30 * scale),
height: 50,
};

dbg!(&rect1);
}

输出结果:

1
2
3
4
5
6
7
8
9
$ cargo run
Compiling rectangles v0.1.0 (file:///projects/rectangles)
Finished dev [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/rectangles`
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
width: 60,
height: 50,
}

The dbg! macro can be really helpful when you’re trying to figure out what your code is doing!

3. 结构体的方法(Method)

方法(method)与函数(function)类似:
- 用fn关键字和名称声明,可以拥有参数和返回值,同时包含在某处调用该方法时会执行的代码。

方法与函数不同之处:

  • 方法在结构体的上下文中被定义(或枚举、trait对象的上下文)
  • 第一个参数总是self,代表调用该方法的结构体实例

3.1. 定义方法

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

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

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

println!(
"The area of the rectangle is {} square pixels.",
rect1.area()
);
}

定义Rectangle结构体的area方法:

  • start an impl (implementation) block for Rectangle
    • Everything within this impl block will be associated with the Rectangle type.
  • area移到impl花括号里面
  • area第1个参数改为self
    • &selfself: &Self的简写,&表示我们借用(borrow)这个Rectangle实例

方法的名字可以和field的名字相同。

3.2. 关联函数(Associated Functions)

所有在impl块中定义的函数被称为关联函数(associated functions),因为它们与impl后面命名的类型相关。

我们可以定义不以self为第一参数的关联函数(因此这个关联函数不是方法),因为它们并不作用于一个结构体的实例。我们已经使用了一个这样的函数:在String类型上定义的String::from函数。

不是方法的关联函数经常被用作返回一个结构体新实例的构造函数

例如:我们可以提供一个关联函数,它接受一个维度参数并且同时作为宽和高,这样可以更轻松的创建一个正方形Rectangle,而不必指定两次同样的值:

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

impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}

fn main() {
let sq = Rectangle::square(3);
}

使用结构体名和::语法来调用这个关联函数:

1
let sq = Rectangle::square(3);

这个函数位于结构体的命名空间中:::语法用于关联函数和模块创建的命名空间。

3.4. 多个impl

每个结构体都允许拥有多个 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
32
33
34
35
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}

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

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

fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};

println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

参考资料

[1] Using Structs to Structure Related Data:https://doc.rust-lang.org/stable/book/ch04-00-understanding-ownership.html