元组

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

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,
}

枚举

枚举 enum通过列举可能的成员来定义一个枚举类型,例如扑克牌花色

1
2
3
4
5
6
enum PokerSuit {
Clubs,
Spades,
Diamonds,
Hearts,
}

枚举值只可能是其中的某一个成员,抽象来看四种花色尽管是不同的花色,但是他们都是扑克花色。

枚举类型是一个类型,包含所有可能的枚举成员,而枚举值是该类型中的具体某个成员的实例

枚举值

现在来创建两个实例

1
2
let heart =  PokerSuit::Hearts;
let diamond = PokerSuit::Diamonds;

我们通过:: 操作符来访问Pokersuit下的具体成员,从代码可以清晰看出heart和 diamond都是Pokersuit枚举类型的,接着可以定义一个函数来使用

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

fn main() {
let heart = PokerSuit::Hearts;
let diamond = PokerSuit::Diamonds;

print_suit(heart);
print_suit(diamond);
}

fn print_suit(suit: PokerSuit) {
match suit {
PokerSuit::Clubs => println!("Clubs"),
PokerSuit::Spades => println!("Spades"),
PokerSuit::Diamonds => println!("Diamonds"),
PokerSuit::Hearts => println!("Hearts"),
}
}

目前来说,枚举值还不能带有值,因此先用就结构体来实现

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

enum PokerSuit {
Clubs,
Spades,
Diamonds,
Hearts,
}

struct PokerCard {
suit: PokerSuit,
value: u8,
}

fn main() {
let c1 = PokerCard {
suit: PokerSuit::Hearts,
value: 1,
};

let c2 = PokerCard {
suit: PokerSuit::Diamonds,
value: 13,
};
}

不过还可以这样

1
2
3
4
5
6
7
8
9
10
11
12
enum PokerCard {
Clubs(u8),
Spades(u8),
Diamonds(u8),
Hearts(u8),
}

fn main() {
let c1 = PokerCard::Hearts(1);
let c2 = PokerCard::Diamonds(13);
}

直接将数据信息关联到枚举成员上,省去将近一半的代码。

不仅如此,同一个枚举类型下的不同成员还能持有不同的数据类型。

1
2
3
4
5
6
7
8
9
10
11
12

enum PokerCard{
Clubs(u8),
Spades(u8),
Diamonds(char),
Hearts(char),
}

fn main(){
let c1 = PokerCard::Spades(5);
let c2 = PokerCard::Diamonds('A');
}

同一化类型

例如我们有一个WEB服务,需要接受用户的长连接,假设连接有两种,TcpStream和TlsStream,但是我们希望对这两个链接的处理流程相同,也就是用同一个函数来处理这两个链接

1
2
3
4
5
6
7
8
9
10

fn new (stream: TcpStream) {
let mut s = stream;
if tls {
s = negotiate_tls(stream);
}
}

websocket = WebSocket::from_raw_socket(
s, ......)

Option 枚举用于处理空值

在其他的编程语言当中,往往都有一个null关键字,用于表面一个变量当前的值为空。

当你对这些null进行操作的时候,例如调用一个方法,就会直接抛出null异常,导致程序的崩溃。

尽管如此,空值的表达依然非常有意义,因为空值表示当前时刻的变量的值是缺失的,有鉴于此,Rust吸取了很多教训,改为使用Option枚举来处理空值。

1
2
3
4
5

enum Option<T> {
Some(T),
None,
}

其中T是泛型参数,Some(T)表示该枚举成员的数据类型是T

1
2
3
4
let some_number = Some(5);
let some_string = Some("a string");

let absent_number: Option<i32> = None;

使用None的时候需要告诉rust OptionT的类型,应为编译器只通过None值无法推断出some成员保存的值的类型。

当有一个Some值的时候,我们就知道存在一个值,而这个值保存在Some中。而使用None的时候,其实和空值的意义是相同的。

换句话说,其实就是因为 Option< T> 和 T的类型实际上是不一样的,在进行T的运算的时候必须把它转换为T类型。

这样就避免了我们错误的使用一个空值。

match语法就是用来进行匹配的。

1
2
3
4
5
6
7
8
9
10
11

fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

数组

在日常开发中最常使用的数据结构之一就是数组。

在Rust中,常用的数组有两种,第一种是速度很快但是长度固定的array, 第二种是动态可增长但是有性能损耗的vector。

在这里,我们先重点看 Array , 将多个类型的相同的元素依次组合在一起

  • 长度固定
  • 元素必须有相同的类型
  • 依次线性排列

数组是Rust的基本类型,是固定长度的,和其他的语言不同,其他编程语言的数组一般是可变长度的。

创建数组

rust中,数组是这样定义的

1
2
3
4
5

fn main() {
let a = [1, 2, 3, 4, 5];
}

array存储在栈上面,性能很优秀

例如要写一年中各个月份的数组

1
2

let months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];

有些时候,还需要声明类型

1
let a: [i32; 5] = [1, 2, 3, 4, 5];

还可以用下面的语法初始化一个值重复出现n次的数组

1
let a = [3; 5]; // 等价于 let a = [3, 3, 3, 3, 3];

访问数组元素

因为数组是连续存放元素的,所以可以通过索引来访问

1
2
3
4
5
6
fn main() {
let a = [9,8,7,6,5];

let first = a[0];
let second = a[1];
}

越界访问

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

use std::io;

fn main() {
let a = [ 1, 2, 3, 4, 5];
println!("Please enter an array index.");

let mut index = String::new();

io::stdin()
.read_line(&mut index)
.expect("Failed to read line");

let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");

let element = a[index];

println!("The value of the element at index {} is: {}", index, element);
}

当你尝试使用索引访问元素时,Rust 将检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度,Rust 会出现 panic。这种检查只能在运行时进行,比如在上面这种情况下,编译器无法在编译期知道用户运行代码时将输入什么值。

数组元素为非基础类型

1
let array = [String::from("Hello"),8];

会直接爆炸

因为let array [ 3;5] 底层是一个个copy出来的,而复杂类型的类型是没有实现Copy特征的,因此无法直接复制。

而如果要手搓,太难看

1
2
3
let array = [String::from("rust is good!"),String::from("rust is good!"),String::from("rust is good!")];

println!("{:#?}", array);

正确的写法应该用

1
2
3
let array: [String; 8] = std::array::from_fn(|_i| String::from("rust is good!"));

println!("{:#?}", array);

数组切片

在之前的章节有讲到过切片

1
2
3
4
5
let a : [i32; 5] = [1, 2, 3, 4, 5];

let slice: &[i32] = &a[1..3];

assert_eq!(slice, &[2, 3]);

slice的类型是&[T],其中T是数组的元素类型。

  • 切片的长度可以不同
  • 创建切片的代价很小
  • 切片类型拥有不固定的大小,而切片引用类型则具有固定的大小
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
fn main() {
// 编译器自动推导出one的类型
let one = [1, 2, 3];
// 显式类型标注
let two: [u8; 3] = [1, 2, 3];
let blank1 = [0; 3];
let blank2: [u8; 3] = [0; 3];

// arrays是一个二维数组,其中每一个元素都是一个数组,元素类型是[u8; 3]
let arrays: [[u8; 3]; 4] = [one, two, blank1, blank2];

// 借用arrays的元素用作循环中
for a in &arrays {
print!("{:?}: ", a);
// 将a变成一个迭代器,用于循环
// 你也可以直接用for n in a {}来进行循环
for n in a.iter() {
print!("\t{} + 10 = {}", n, n+10);
}

let mut sum = 0;
// 0..a.len,是一个 Rust 的语法糖,其实就等于一个数组,元素是从0,1,2一直增加到到a.len-1
for i in 0..a.len() {
sum += a[i];
}
println!("\t({:?} = {})", a, sum);
}
}
  • 数组类型容易跟数组切片混淆,[ T;n] 描述了一个数组的类型,而 [ T] 描述了切片的类型, 因为切片是运行期的数据结构,它的长度无法在编译期得知,因此不能用 [ T;n] 的形式去描述
  • [u8; 3]和[u8; 4]是不同的类型,数组的长度也是类型的一部分
  • 在实际开发中,使用最多的是数组切片[ T],我们往往通过引用的方式去使用&[ T],因为后者有固定的类型大小