元组

元组是多种类型组合到一起形成的,因此它是复合类型,元组的长度是固定的,元组的顺序也是固定的。

1
2
3
fn main() {
let tup: (i32, f64, u8) = (500, 6.4, 1);
}

变量tpu被绑定了一个元组值 (500,6.4,1), 该元组的类型是(i32, f64, u8)

元组使用括号将多个类型组合到一起。

用模型匹配解构元组

1
2
3
4
5
6
7
fn main() {
let tup = (500, 6.4, 1);

let (x,y,z) = tup;

println!("The value of y is: {}", y);
}

上述代码首先创建一个元组,然后帮他绑定到tup上面,然后用 let( x,y,z) = tup来完成模式匹配。。

用 . 来访问元组

模式匹配可以让我们一次性的把元组中的值取出来,但是如果只是想要访问特定的元素,那么模式匹配就略显繁琐,对此,Rust提供了.的访问方式:

1
2
3
4
5
6
7
8
9
10

fn main(){
let x: (i32, f64, u8) = (500, 6.4, 1);

let five_hundred = x.0;

let six_point_four = x.1;

let one = x.2;
}

元组的使用示例

元组在函数返回值非常有用,例如和下面的代码,可以使用元组返回多个值

1
2
3
4
5
6
7
8
9
10
11
12
13
fn main() {
let s1 = String::from("hello");

let (s2, len) = calculate_length(s1);

println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
let length = s.len(); // len() 返回字符串的长度

(s, length)
}

calculate_length 函数接收 s1 字符串的所有权,然后计算的字符串的长度,接着把字符串所有穿和字符串长度再返回给s2 和 len 变量

结构体

结构体 struct 恰恰就是复合数据解构,不过可能有不同的名称

结构体语法

定义结构体

  • 通过关键字 struct 定义
  • 一个清晰明确的结构体 名称
  • 几个有名字的结构体 字段

例如:

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

该结构体名称是User,拥有四个字段,且每个字段都有对应的字段名及其类型声明,例如Username 代表用户名,类型是string

创建结构体实例:

1
2
3
4
5
6
7

let userl = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};

有几点值得注意:

  1. 初始化实例的时候,每个字段都要进行初始化
  2. 初始化时的字段顺序不需要和结构体定义的顺序一致

访问结构体字段:

1
2
3
4
5
6
7

let mut userl = User {
email: String::from("someone@example.com"),
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};

需要注意的是,必须要将结构体实例声明为可变的,才能修改其中的字段,Rust不支持将某个结构体的某个字段标记为可变。

简化结构体创建:

构建函数

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

他接受两个字符串参数 email 和 username,然后创建一个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,
}
}

因为上面和下面有一样的字段名,所以可以通过略缩的方式初始化,和Typescript的方式差不多。

结构体更新语法:

在实际场景中,有一种情况很常见: 根据已有的结构体创建新的结构体

1
2
3
4
5
6
7

let user2 = User {
active : user1.active,
username: user1.username,
email: String::from("another@email.com"),
sign_in_count: user1.sign_in_count,
};

同时,Rust还有结构体更新语法

1
2
3
4
5

let user2 = User {
email: String::from("another@example.com"),
..user1
};

因为user2仅仅在email上面和user1不同,因此我们只需要对 email进行赋值。剩下的通过结构体更新语法 ..user1 即可完成。

.. 语法表明是凡是没有声明的字符都从user1里面获取

..user1 必须在结构体的尾部使用

结构体更新语法跟赋值语句 = 很类似。因此在上面代码中 user1的部分字段所有权被转移到了user2中,Username字段发生了所有权转移。作为结构,User1无法再被使用。

在上面,只有username发生了所有权转移,因为username是string类型,没有实现Copy特征,因此发生了所有权转移。而active和sign_in_count是基本类型,发生了拷贝。

user1无法再被使用,但是user1内部的其他字段还是能正常用的。

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

# #[derive(Debug)]
# 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,
};
let user2 = User {
active: user1.active,
username: user1.username,
email: String::from("anohter@example.com"),
sign_in_count: user1.sign_in_count,
};

println!("{}", user1.active); // true

println!("{:?}", user1); // error
#}

结构体中的内存排列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

#[derive(Debug)]

struct File{
name: String,
data: Vec<u8>,
}

fn main(){
let f1 = File {
name: String::from("f1.txt"),
data: Vec::new(),
};

let f1_name = &f1.name;
let f1_length = &f1.data.len();

println!("{:?}", f1);
println!("{} is {} bytes long", f1_name, f1_length);
}

上述的File结构体在内存中,其name和data两个字段分别拥有底层量[u8] 数组的所有权 (String 类型的底层也是[u8]数组), 通过ptr 指针指向底层数组的内存地址.

把结构体中具有所有权的字段转移出去后,就无法访问该字段,但是可以正常访问其它的字段.

元组结构体

结构体必须要有名称,但是结构体的字段可以没有名称。这种结构类似于元组。所以被叫做元组结构体.

1
2
3
4
5
6

struct Color(i32, i32, i32);
struct Point(i32, i32, i32);

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

单元结构体(Unit-like Struct)

单元结构体和单元类型类似,没有任何字段和属性。

如果我们定义一个类型,但是不关心其内容,只关心行为,就可以用单元结构体

1
2
3
4
5
6
7
8
9

struct AlwaysEqual;

let subject = AlwaysEqual;
// 我们不关心 AlwaysEqual 的字段数据,只关心它的行为,因此将它声明为单元结构体,然后再为它实现某个特征

impl SomeTrait for AlwaysEqual {
// 方法实现
}

结构体数据的所有权

在之前User 结构体的定义中,有一处细节,我们使用了自身拥有所有权的String类型而不是基于引用的&str类型。

因为我们想要这个结构体拥有它所有的数据,而不是从其他地方借用数据。

你也可以让User结构体从其他地方借用数据。

这种东西就需要引入 生命周期 的概念了。 生命周期能够确保结构体的作用范围要比它所借用的数据的作用范围要小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

struct User {
username: &str,
email: &str,
sign_in_count: u64,
active: bool,
}

fn main() {
let userl = User {
email: "someone@example.com",
username: "someusername123",
active: true,
sign_in_count: 1,
};
}

这个代码会导致报错.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
error[E0106]: missing lifetime specifier
--> src/main.rs:2:15
|
2 | username: &str,
| ^ expected named lifetime parameter // 需要一个生命周期
|
help: consider introducing a named lifetime parameter // 考虑像下面的代码这样引入一个生命周期
|
1 ~ struct User<'a> {
2 ~ username: &'a str,
|

error[E0106]: missing lifetime specifier
--> src/main.rs:3:12
|
3 | email: &str,
| ^ expected named lifetime parameter
|
help: consider introducing a named lifetime parameter
|
1 ~ struct User<'a> {
2 | username: &str,
3 ~ email: &'a str,
|

使用#[derive(Debug)]打印结构体

在前面的代码中我们使用 #[derive(Debug)]对结构体进行了标记,这样才能使用 println!("{:?",s};) 的方式对其进行打印输出。

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

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

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

首先可以观察到上面使用了 {} 而不是 {:?}, 运行后报错

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

这提示我们结构体上没有实现 Display 特征。

报错提示我们使用 {:?}.

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

但是还是报错

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

不过编译器让我们去实现Debug特征, 不过我们也可以直接
add #[derive(Debug)] to Rectangle 来让编译器帮我们实现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!("rectl is {:?}", rect1);
}

此时就能够正常打印了。

1
2
$ cargo run
rect1 is Rectangle { width: 30, height: 50 }

当结构体比较大的时候,我们可能希望能够有更好的输出表现,此时可以用{:#?}来代替{:?},这样就会有更好的输出表现。

1
2
3
4
rect1 is Rectangle {
width: 30,
height: 50,
}

如果还是不满意的哈,可以实现Display特征,以向用户更美的展示结构体。

当然,还可以使用 dbg!() 宏来打印结构体,dbg!() 宏会打印出文件名和行号,方便调试。

它会拿走表达式的所有权,然后打印文件名、行号、debug信息,当然还有需要的表达式结果。最后会把表达式的所有权返回给调用者。

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
$ cargo run
[src/main.rs:10] 30 * scale = 60
[src/main.rs:14] &rect1 = Rectangle {
width: 60,
height: 50,
}