一、常见cargo命令
- 查看cargo版本 cargo --version
- 创建cargo项目 create new demo_name
- 构建编译项目 cargo build
- 运行项目 cargo run
- 检查项目代码 cargo check (比cargobuild快)
- 发布构建项目 cargo build --release
二、小demo-----猜数游戏
1、print
mut 将 "不可变值" 变为 "可变值"
rust
将a的值赋值给b
let a = 1;
let b = a;
println!("{}",a);//输出宏的传参
println!("{}",b);
输出:
1
1
如果用另一种方式,先声明,再赋值
let a = 1;
let b = 0;
b = a;
会报错,因为所有的声明,默认是 immutable 不可变的
需要加 mut
let a = 1;
let mut b = 0;
b = a;
println!("{}",a);
println!("{}",b);
注意:不能 let b ,所有的声明都要有初始值
读取输入
rust
io::stdin().read_line(&mut guess).expect("无法读取行");
如果read_line()
返回的io::Result
实例的值是Ok ,那么expect()
会提取Ok的附加值,作为结果返回给用户
如果read_line()
返回的io::Result
实例的值是Err ,那么expect()
会把程序中断,并把传入的字符串信息显示出来
rust
use std::io;
fn main() {
println!("猜数");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("无法读取行");
println!("你猜测的数是{}",guess);
}
2、随机数
在Cargo.toml 文件中引入生成随机数的包(相当于java中maven坐标导入)
text
[dependencies]
rand = "^0.3.14"
^表示任何与这个版本公共API兼容的版本都可以
随后重新启动包:
Ctrl + Shift + P
>rust
Rust: Start the Rust server
代码
rust
use std::io;
use rand::Rng;
fn main() {
println!("猜数!!!");
// 左闭右开
let secret_number = rand::thread_rng().gen_range(1,101);
println!("生成随机数:{}",secret_number);
println!("请输入你要猜的数字:");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("无法读取行");
println!("你猜测的数是{}",guess);
}
3、比较生成的随机数和输入数字
rust
use std::io;
use rand::Rng;
use std::cmp::Ordering;
fn main() {
println!("猜数!!!");
let secret_number = rand::thread_rng().gen_range(1,101);
println!("生成随机数:{}",secret_number);
println!("请输入你要猜的数字:");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("无法读取行");
println!("你猜测的数是{}",guess);
// 将字符串类型转化为u32类型
let guess: u32 = guess.trim().parse().expect("请输入整数");
match guess.cmp(&secret_number){
Ordering::Less => println!("太小了"),
Ordering::Greater => println!("太大了"),
Ordering::Equal => println!("猜对了"),
}
}
4、多次猜测
rust
use std::io;
use rand::Rng;
use std::cmp::Ordering;
fn main() {
println!("猜数!!!");
let secret_number = rand::thread_rng().gen_range(1,101);
println!("生成随机数:{}",secret_number);
loop{
println!("请输入你要猜的数字:");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("无法读取行");
println!("你猜测的数是{}",guess);
// 防止输入的是非数字
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
match guess.cmp(&secret_number){
Ordering::Less => println!("太小了"),
Ordering::Greater => println!("太大了"),
Ordering::Equal => {
println!("猜对了");
break;
}
}
}
}
四、通用的编程概念
1、变量与可变性
变量:
rust
let a = 1; // 用let修饰,此时是不可以修改的
let mut ab = 1; // 加上mut之后,变量可以进行修改
常量:(大写字母+数字下划线)
rust
const MAX_POINTS: u32 = 10_0000;
fn main() { const MAX_POINTS: u32 = 10_0000; }
Shadowing
可以使用相同的名字声明新的变量,新的变量就会 shadow(隐藏) 之前声明的同名变量
在后续的代码中这个变量名就是新的变量
shadow :
rust
fn main() {
let a = 2;
let a = a + 1;
let a = a * 3;
println!("The value a is :{}",a);
}
mut
:
rust
fn main() {
let mut a = 2;
a = a + 1;
a = a * 3;
println!("The value a is :{}",a);
}
2、数据类型
Rust 是静态编译语言,在编译时必须知道所有变量的类型
-
基于使用的值,编译器通常能够推断出它的具体类型 Ctrl+左键 查看示例
-
但如果可能的值比较多(例如把
String
转成整数的parse()
方法),就必须添加类型的标注,否则编译会报错
rust
let str: u32 = "2".parse().expect("Not a number");
1)标量类型
一个标量类型代表一个单个的值
Rust 有四个主要的标量类型:
整数类型
如表格所示,每种都分 i 和 u 以及固定的位数
位长度 | 有符号 | 无符号 |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
浮点类型
Rust 有两种基础的浮点类型
f32
,32位,单精度
f64
,64位,双精度
rust
let a = 1.0; //f64
let a :f32 = 1.0; //f32
Rust 的浮点类型使用了 IEEE-754 标准来表述
默认会使用 f64
类型
数值操作
加减乘除余
跟其他语言一样,不多赘述
rust
let sum = 5 + 10;
let difference = 97.8 - 24.1;
let producet = 4 * 30;
let quotient = 56.7 / 32.1;
let reminder = 54 % 5;
布尔类型
- Rust 的布尔类型:
true
、false
- 1个字节大小
- 符号是
bool
rust
let t = true;
let f :bool = false;
字符类型
请注意字符类型是单引号
如果let b :char = "₦"
这样声明会报错
rust
let a = 'n';
let b :char = '₦';
let c = ' ';
2)复合类型
- 复合类型可以将多个值放在一个类型里
- Rust 提供了两种基础的复合类型:元组(Tuple)、数组
元组(Tuple)
- Tuple 可以将多个类型的多个值放在一个类型里
- Tuple 的长度是固定的:一旦声明就无法改变
创建Tuple
- 在小括号里,将值用逗号隔开
- Tuple 中的每个位置都对应一个类型,Tuple中各元素的类型不必相同
rust
let tup: (i32,f64,char) = (100,5.1,'a');//创建Tuple
println!("{},{},{}",tup.0,tup.1,tup.2);
获取Tuple的元素值
- 可以使用模式匹配来解构(destructure)一个Tuple 来获取元素的值
rust
let tup: (i32,f64,char) = (100,5.1,'a');
let (x, y, z) = tup;//给变量赋值
println!("{},{},{}", x, y, z);
访问Tuple的元素
- 在 tuple 变量使用点标记法,后接元素的索引号
rust
let tup: (i32,f64,char) = (100,5.1,'a');
println!("{},{},{}",tup.0,tup.1,tup.2);//访问 Tuple 的元素
数组
rust
let a = [1, 2, 3, 4];
3、函数和注释
1)函数
rust
fn main() {
another_function(5, 6);
}
fn another_function(x: i32, y: i32) {
println!("the value of x is : {}, y is : {}", x, y);
}
函数体中的语句和表达式
**例子:**这个 y
代码块中的最后一行,不带分号表示的就是表达式 ,y
的值就是这个块中最后一个表达式的值
如果你把x + 3
加了一个分号变为这样,x + 3;
,会报错,y
的值是()
rust
fn main() {
let x = 5;
let y = {
let x = 1;
x + 3
};
println!("{}", y);//输出 4
}
函数的返回值
rust
fn main() {
println!("{}", f(3));
}
fn f(x: i32) -> i32 {
x + 5
}
返回多个值
rust
fn main() {
let (p2,p3) = pow_2_3(789);
println!("pow 2 of 789 is {}.", p2);
println!("pow 3 of 789 is {}.", p3);
}
fn pow_2_3(n: i32) -> (i32, i32) {
(n*n, n*n*n)
}
2)注释
跟正常的语言一样
rust
/*
*注释
*/
//注释
4、控制流
1)if
else
rust
fn main() {
let a = 3;
if a < 3 {
println!("a < 3");
} else if a == 3 {
println!("a = 3");
} else {
println!("a > 3");
}
}
- 如果使用了多于一个的
else if
,那么最好使用match
来重构代码
rust
use std::cmp::Ordering;
fn main() {
let a = 3;
match a.cmp(&3) {
Ordering::Less => println!("a < 3"),
Ordering::Greater => println!("a > 3"),
Ordering::Equal => println!("a = 3"),
}
}
- 在
let
语句中使用if
- 因为
if
是一个表达式,所以可以将它放在let
语句中等号的右边
rust
fn main() {
let a = true;
let number = if a { 5 } else { 6 };
println!("{}", number);
}
if
和else
返回值类型必须相同,因为Rust 要求每个if
else
中可能成为结果的返回值类型必须是一样的,为了安全,编译时就要确定类型
2)循环
loop
loop
关键字告诉 Rust 反复的执行一块代码,直到你喊停- 可以在
loop
循环中使用break
关键字来告诉程序何时停止循环
rust
fn main() {
let mut a = 0;
let result = loop {
a += 1;
if a == 10 {
break a * 2;
}
};
println!("{}", result);
}
while
- 每次执行循环体之前都判断一次条件
rust
fn main() {
let mut number = 3;
while number != 0 {
println!("{}", number);
number -= 1;
}
println!("结束")
}
for
- while 和 loop 也能实现遍历循环,但是for 不需要写判断,也就不会出现数组越界的情况,还有就是速度比较快,所以 Rust 用来遍历的还是用
for
rust
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("value is {}", element);
}
}
- 把数组中的值,同步加50
rust
fn main() {
let mut v = [100, 32, 57];
for i in &mut v{
*i += 50;
}
for i in v{
println!("{}",i);
}
}
输出:
rust
150
82
107
五、所有权
所有权是 Rust 最独特的特性,它让 Rust 无需 GC 就保证内存安全
1、什么是所有权
-
Rust 的核心特性就是所有权
-
所有程序在运行时都必须管理它们使用计算机内存的方式
-
有些语言有垃圾收集机制,在程序运行时,它们会不断寻找不再使用的内存(C#、Java)
-
在其他语言中,程序员必须显式的分配和释放内存(C、C++)
-
Rust 采用了第三种方式
-
内存是通过一个所有权系统来管理的,其中包含一组编译器在编译时检查的规则
-
当程序运行时,所有权特性不会减慢程序的运行速度(因为都在编译期完成了)
2、Stack and Heap(栈内存和堆内存)
- 在像 Rust 这样的系统级编程语言中,一个值是在 stack 上还是在 heap 上对语言的行为和你为什么要做某些决定是由更大的影响的
- 在你的代码运行的时候,Stack 和 Heap 都是你可用的内存,但他们的结构很不相同
1)存储数据
- Stack 会按值的接收顺序来存储,按相反的顺序将它们移除(先进后出,后进先出)
- 因为指针是已知固定大小的,可以把指针存放在 Stack 上(也就是说,Stack 存储着 Heap 的指针)
- 如果想要实际的数据,你必须使用指针来定位
- 把数据压到 Stack 上要比在 Heap 上分配快得多
- 因为(入栈时)操作系统无需为存储新数据去搜索内存空间;其位置总是在栈顶。
- 相比之下,在堆上分配内存则需要更多的工作,这是因为操作系统必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。
2)访问数据
- 访问 Stack 中的数据要比访问 Heap 中的数据要快,因为需要通过指针才能找到 Heap 中的数据
- 处理器在处理的数据彼此较近的时候(比如在栈上),比较远的时候(比如在堆上)能更快
3)函数调用
- 当你的代码调用函数时,值被传入函数(也包括指向 Heap 的指针)。函数本地的变量被压到 Stack 上,当函数结束后,这些值会从Stack上弹出
3、所有权存在的原因
- 所有权解决的问题:
- 跟踪代码的哪些部分正在使用 Heap 的哪些数据
- 最小化 Heap 上的重复数据量
- 可以清理 Heap 上未使用的数据以避免空间不足
- 一旦你懂得了所有权,那么就不需要经常去想 Stack 或 Heap 了
- 但是知道管理 Heap 数据是所有权存在的原因,这也有助于理解它为什么会这样工作
4、所有权的规则
- 每个值都有一个变量,这个变量是该值的所有者
- 每个值同时只能有一个所有者
- 当所有者超出作用域(scope)时,该值将被删除
1)变量作用域(Scope)
- Scope 就是程序中一个有效范围
- 跟别的语言一样
rust
fn main() {
//s 不可用
let s = "hello";//s 可用
//可以对 s 进行相关操作
}//s 作用域到此结束,s 不可用
2)String 类型
-
String 类型比那些基础标量数据类型更复杂
-
基础数据类型存放在 Stack 上,离开作用域就会弹出栈
-
我们现在用一个存储在 Heap 上面的类型,来研究 Rust 是如何回收这些数据的
-
String 会在 Heap 上分配,能够存储在编译时未知数量的文本
创建 String 类型的值
- 可以使用
from
函数从字符串字面值创建出 String 类型 let mut s = String::from("hello");
::
表示from
是 String 类型下的函数- 这类字符串是可以被修改的
rust
fn main() {
let mut s = String::from("hello");
s.push_str(" word");
println!("{}", s);
}
5、所有权与函数(例子)
- 在语义上,将值传递给函数和把值赋给变量是类似的
- 将值传递给函数将发生移动 和复制
例子:
rust
fn main() {
let s = String::from("Hello World");//这里声明引用类型,String,
take_ownership(s);//放入函数,发生了移动
let a = 1;//声明整型
makes_copy(a);//实际上传入的是a的副本
}//a:在Stack中的本来数据被drop
fn take_ownership(some_string: String) {
println!("{}", some_string);
}//s:这里Heap中的数据被drop了
fn makes_copy(some_number: u32) {
println!("{}", some_number);
}//a:在Stack中的副本数据被drop
6、返回值与作用域
函数在返回值的过程中同样也会发生所有权的转移
rust
fn main() {
let s1 = gives_ownership(); //返回值的所有权转移给s1 发生了移动
let s2 = String::from("hello");
let s3 = takes_and_gives_back(s2);//s2 所有权移交给这个方法,然后又移交给s3
}
fn gives_ownership() -> String {
let some_string = String::from("hello");
some_string
}
fn takes_and_gives_back(a_string: String) -> String {
a_string
}
- 一个值赋给其他变量时就会发生移动
- 当一个包含 Heap 数据的变量离开作用域时,它的值就会被
drop
函数清理,除非数据所有权移动到另一个变量上了
7、让函数使用某个值,但不获得所有权
rust
fn main() {
let s1 = String::from("hello");
let (s2, len) = calculate_length(s1);//把s1的所有权移交到,这个方法中的s,然后再返回
println!("The length of '{}' is {}", s2, len);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();//这个length是usize类型,基础类型,存储在Stack中
(s, length)//这里length返回一个副本就可以了
}
这种做法,不得不把变量作为参数传入,然后又作为返回值传出,很繁琐
- 针对这个场景,Rust有一个特性,叫做 "引用" (Reference)
六、引用和借用
1)引用
rust
fn main() {
let s1 = String::from("Hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.len()
}
- 参数的类型是
&String
而不是String
&
符号就表示引用:允许你引用某些值而不取得其所有权
2)借用
当一个函数使用引用,而不是一个真实的值作为它的参数,我们就管这个行为叫做借用
修改借用的数据
那我们是否可以修改借用的东西呢?
-
不可以
-
和变量一样,引用默认也是不可变的
rust
fn main() {
let s1 = String::from("Hello");
let len = calculate_length(&s1);
println!("The length of '{}' is {}", s1, len);
}
fn calculate_length(s: &String) -> usize {
s.push_str(",World");//这里会报错
s.len()
}
那么我们把引用的变为可变的,是否就可以修改了呢
这样就不会报错了
rust
fn main() {
let mut s1 = String::from("Hello");
let len = calculate_length(&mut s1);
println!("The length of '{}' is {}", s1, len);
}
fn calculate_length(s: &mut String) -> usize {
s.push_str(",World");
s.len()
}
修改借用的数据时的限制
但是有个重要的限制:在特定作用域内,对某一块数据,只能有一个可变的引用
- 这样做的好处就是可以在编译时防止数据竞争
- 以下三种行为会发生数据竞争:
- 两个或多个指针同时访问一个数据
- 至少有一个指针用于写入数据
- 没有使用任何的机制来同步对数据的访问
rust
fn main() {
let mut s = String::from("Hello");
let s1 = &mut s;
let s2 = &mut s;//这里会报错告诉你只能用一个
println!("{}, {}", s1, s2);
}
我们可以通过创建新的作用域,来允许非同时的创建多个可变引用
就像这样
rust
fn main() {
let mut s = String::from("Hello");
{
let s1 = &mut s;//s1 就会在这个作用域存在
}
let s2 = &mut s;
}
还有另一个限制
- 不可以同时拥有一个可变引用和一个不变的引用
- 多个不变的引用是可以的
例子:
rust
fn main() {
let mut s = String::from("Hello");
let s1 = &s;//这里是不变引用
let s2 = &s;
let r = &mut s;//这里是可变引用就报错了
println!("{},{},{}", s1, s2, r);
}
悬垂引用(Dangling References)
-
在具有指针的语言中可能会有一个错误叫做悬垂指针(*dangling pointer*)
-
悬垂指针:一个指针引用了内存中的某个地址,而这块内存可能已经释放分配给其他人使用了
-
在 Rust 中,编译器可保证引用永远不会处于悬垂状态
-
我们尝试创建一个悬垂引用,Rust 会通过一个编译时错误来避免:
rust
fn main() {
let r = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}//离开这个方法作用域,s销毁了,而这个方法,返回了s的引用,也就是说,会指向一个已经被释放的内存空间,所以会直接报错
七、切片
rust
fn main() {
let s = String::from("Hello World");
let hello = &s[0..5];
let world = &s[6..11];
println!("{}", hello);
println!("{}", world);
}
通过这样的方式进行截取&s[0..5]
表示引用 [ 0, 5 )
左闭右开,内部数字为引用字符串索引
rust
fn main() {
let str = String::from("Hello World");
let r = first_word(&str);
str.clear();//这里会报错
println!("{}", r) //输出结果是5,空格所在位置的索引
}
因为在这里,let r = first_word(&str);
用的是不可变引用
这里str.clear();
用的是可变引用
八、Struct
1)定义并实例化struct
rust
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
实例化struct
rust
let user1 = User{
email:String::from("1841632321@qq.com"),
username:String::from("李泽辉"),
sign_in_count:1,
active:true,
}
取得 struct 里面的某个值
- 使用点标记法
rust
println!("{}", user1.username);
println!("{}", user1.email);
println!("{}", user1.sign_in_count);
println!("{}", user1.active);
修改 struct 里面的某个值
- 修改了
username
,注意要给实例user1
加mut
因为是可变的 - 而一旦这个实例
user1
是可变的,那么示例中的所有字段都是可变的
rust
let mut user1 = User {
email: String::from("1841632321@qq.com"),
username: String::from("李泽辉"),
sign_in_count: 1,
active: true,
};
struct 作为函数的返回值
- 传入用户名和邮箱,返回用户的结构体
rust
fn return_user(email: String, username: String) -> User {
User {
email: email,
username: username,
sign_in_count: 1,
active: true,
}
}
字段初始化简写
- 当字段名与字段值对应的变量名相同时,就可以使用字段初始化简写的方式
- 比如上面的例子 ,传入用户名和邮箱,结构体字段名和传入字段名一样,可以直接简写为下面的样子
rust
fn return_user(email: String, username: String) -> User {
User {
email,
username,
sign_in_count: 1,
active: true,
}
}
struct 更新语法
- 当你想基于某个 struct 实例来创建一个新实例的时候,可以使用 struct 更新语法:
rust
fn main() {
let user1 = User {
email: String::from("1841632321@qq.com"),
username: String::from("李泽辉"),
sign_in_count: 1,
active: true,
};
let user2 = User {
email: String::from("新邮箱"),//改变了email
..user1//其他的不改变可以直接这样写,表示这个新实例中剩下的没被赋值的字段(除了email)和user1的一样
};
}
Tuple Struct
rust
fn main() {
struct Color(u8, u8, u8);
struct Point(f64, f64);
let black = Color(0, 0, 0);
let origin = Point(0.1, 0.2);
println!("black = ({}, {}, {})", black.0, black.1, black.2);
println!("origin = ({}, {})", origin.0, origin.1);
}
-
运行结果:
black = (0, 0, 0)
origin = (0.1, 0.2)
2)struct 的例子
例子需求
计算长方形面积
rust
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rec = Rectangle {
width: 30,
height: 50,
};
println!("面积是{}", area(&rec));
}
fn area(rectangle: &Rectangle) -> u32 {
rectangle.width * rectangle.height
}
struct 上面的方法
rust
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn area(&self) -> u32 {
self.width * self.height
}
}
fn main() {
let rec = Rectangle {
width: 30,
height: 50,
};
println!("面积是{}", rec.area());
}
带有更多参数的方法
我们要实现一个功能,判断一个长方形,是否能容纳下另一个长方形
rust
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
fn main() {
let rec1 = Rectangle {
width: 30,
height: 50,
};
let rec2 = Rectangle {
width: 10,
height: 40,
};
let rec3 = Rectangle {
width: 35,
height: 55,
};
println!("rec1能否包括rec2:{}", rec1.can_hold(&rec2));//调用can_hold时候,&self代表rec1,other代表rec2
println!("rec1能否包括rec3:{}", rec1.can_hold(&rec3));
}
返回:
rust
rec1能否包括rec2:true
rec1能否包括rec3:false
关联函数
rust
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let r = Rectangle::square(20);
println!("{:#?}", &r);//输出一下
}
九、枚举与模式匹配
1)枚举
rust
enum ipAddrKind {
V4,
V6,
}
fn main() {
let four = ipAddrKind::V4;
let six = ipAddrKind::V6;
route(four);
route(six);
route(ipAddrKind::V4);
route(ipAddrKind::V6);
}
fn route(ip_kind: ipAddrKind) {}
将数据附加到枚举的变体中
所有类型都可以进行附加数据
rust
enum Message {
Quit,//匿名结构体
Move { x: i32, y: i32 },//坐标结构体
Write(String),//字符串
ChangeColor(i32, i32, i32),//元组
}
fn main() {
let q = Message::Quit;
let m = Message::Move { x: 10, y: 22 };
let q = Message::Write(String::from("字符串"));
let q = Message::ChangeColor(0, 255, 255);
}
2)Option 枚举
- Rust 没有 Null ,所以Rust 提供了类似 Null 概念的枚举 -
Option<T>
- 定义于标准库中
- 在 Prelude(预导入模块)中
- 描述了:某个值 可能存在(某种类型)或不存在的情况
rust
enum Option<T> {
Some(T),
None,
}
可以直接使用,不需要像正常的枚举一样,Option::Some(5);
rust
let some_number = Some(5); //std::option::Option<i32>
let some_string = Some("A String"); //std::option::Option<&str>
let absent_number: Option<i32> = None;//这里编译器无法推断类型,所以要显式的声明类型
如果你想针对 opt 执行某些操作,你必须先判断它是否是 Option::None:
rust
fn main() {
let opt = Option::Some("Hello");
//let opt: Option<&str> = Option::None;
//let opt: Option<&str> = None;
//空值
match opt {
Option::Some(something) => {
println!("{}", something);
},
Option::None => {
println!("opt is nothing");
}
}
}
运行结果:
rust
Hello
//opt is nothing
3)控制流运算符 - match
- 允许一个值与一系列模式进行匹配,并执行匹配的模式对应的代码
- 模式可以是字面值、变量名、通配符
有一个结构体Coin
里面四个变体,对应四个分支返回值
rust
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
//进行匹配
match coin {
Coin::Penny => {
println!("{}", 1);
1
}
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => 25,
}
}
fn main() {
value_in_cents(Coin::Penny);
}
输出:
rust
1
绑定值的模式匹配(提取enum中的变体)
- 匹配的分支可以绑定到被匹配对象的部分值
- 因此可以从 enum 变体中提取值
rust
#[derive(Debug)]
enum UsState {
Alabama,
Alaska { x: u32, y: u32 },
}
enum Coin {
Penny,
Nickel,
Dime { index: u8 },
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
//匹配
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime { index } => 10,
Coin::Quarter(state) => {
println!("state is {:#?}", state);
25
}
}
}
fn main() {
let c = Coin::Quarter(UsState::Alaska { x: 10, y: 20 }); //传值
let x = Coin::Dime { index: 2 };
println!("{}", value_in_cents(c)); //取值
println!("{}", value_in_cents(x)); //取值
}
输出:
rust
state is Alaska {
x: 10,
y: 20,
}
25
10
匹配Option<T>
rust
fn main() {
let five = Some(5); //定义一个Option
let six = plus_one(five); //走Some分支,i+1
let none = plus_one(None); //为None返回None
}
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
match必须穷举所有可能
Option有两个变体,一个None一个Some
必须都有分支
rust
fn main() {}
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
match使用通配符_
不用穷举所有可能性了
rust
fn main() {
let v = 4;
match v {
1 => println!("1"),
3 => println!("2"),
_ => println!("other"),
}
}
_
表示除了以上两种情况外,剩下所有的
if let
处理只关心一种匹配,忽略其他匹配的情况,你可以认为他是只用来区分两种情况的match
语句的语法糖
语法格式:
rust
if let 匹配值 = 源变量 {
语句块
}
用match
来写,如果i
是0
,输出0
,其他数字输出other
rust
fn main() {
let i = 0;
match i {
0 => println!("zero"),
_ => println!("other"),
}
}
我们用if let
试一下
rust
fn main() {
let i = 0;
if let 0 = i {
println!("zero")
} else {
println!("other")
}
}
输出:
rust
zero
上面的是标量,我们现在用枚举试一下
rust
fn main() {
enum Book {
Papery(u32),
Electronic,
}
let book = Book::Papery(1);
if let Book::Papery(index) = book {
println!("{}", index)
} else {
println!("Electronic")
}
}
输出:
rust
1
十、Package,Crate,Module
-
模块系统:
-
Package(包):Cargo的特性,让你构建、测试、共享 Crate
-
Crate(箱):一个模块树(当你要编译时,你要编译的那个文件就叫crate),它可以编译生成一个 二进制文件 或 多个库文件
-
Module(模块)、use:让你控制代码的组织、作用域、私有路径
-
Path(路径):为struct、function、module等项命名的方式
1)Package 与 Crate
Crate 的类型有两种:
- binary crate(二进制)编译后产生二进制文件的源文件就叫 binary crate
- library crate(库)编译后产生二进制文件的源文件就叫 library crate
Crate Root(Crate 的根):
- 是源代码文件
- Rust 编译器从这里开始,如果里面含有
mod
声明,那么模块文件的内容将在编译之前被插入 crate 文件的相应声明处
一个Package:
- 包含一个 Cargo.toml,它描述了如何构建这些Crates
- 只能包含 0-1 个 library crate
- 可以包含任意数量的 binary crate
- 但至少包含一个 crate (library 或 binary)
2)Cargo的惯例
一个例子:
我们创建一个新的项目(一个项目就是一个包)
cargo new my-project1
官方文档:src/main.rs ,是一个与包同名的 binary crate 的 crate 根
解释:src/main.rs 被Cargo 传递给编译器 rustc
编译后,产生与包同名的二进制文件
cargo new --lib my-project2
官方文档:src/lib.rs,是与包同名的 library crate 的 crate 根
解释:src/lib.rs 被Cargo 传递给编译器 rustc
编译后,产生与包同名的库文件
Cargo会默认把这个文件作为根
- 如果一个Package 同时包含src/main.rs 和 src/lib.rs
- 那就说明它有一个 binary crate 一个 library crate
- 一个Package有多个binary crate 的情况下
- 文件要放在 src/bin 下
- 每个文件都是单独的 binary crate
3)定义 Module 来控制作用域和私有性
- Module
- 在一个 crate 内,将代码进行分组
- 增加可读性,易于复用
- public private
建立Mudule:
cargo new --lib module
在 lib.rs 文件中写入module
我们定义一个模块,是以 mod
关键字为起始,然后指定模块的名字(本例中叫做 front_of_house
),并且用花括号包围模块的主体。在模块内,我们还可以定义其他的模块,就像本例中的 hosting
和 serving
模块。模块还可以保存一些定义的其他项,比如结构体、枚举、常量、特性、或者函数。
rust
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn server_order() {}
fn take_payment() {}
}
}
在前面我们提到了,src/main.rs
和 src/lib.rs
叫做 crate 根。之所以这样叫它们的原因是,这两个文件的内容都是一个从名为 crate
的模块作为根的 crate 模块结构,称为 模块树 (module tree)。这个就是lib.rs的模块树
rust
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
4)路径PATH
-
为了在Rust的模块中找到某个条目,需要使用路径
-
路径的两周形式
-
绝对路径:从 crate root 开始,使用 crate 名 或 字面值 crate
-
相对路径:从当前模块开始,使用 self 、super 或当前模块的标识符
-
路径至少由一个标识符组成,标识符之间使用 ::
-
如果定义的部分和使用的部分总是一起移动,用相对路径,可以独立拆解出来,用绝对路径
例子:
rust
mod front_of_house {
mod hosting {
fn add_to_waitlist() {
println!("1111");
}
}
}
fn main() {
crate::front_of_house::hosting::add_to_waitlist();//绝对路径
front_of_house::hosting::add_to_waitlist();//相对路径
}
super的用法
rust
fn serve_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::serve_order();
}
fn cook_order() {}
}
用super
表示所在代码块的父级,
也就是fix_incorrect_order
的父级mod back_of_house
,然后在这个目录下去找到serve_order
方法
pub struct
rust
mod back_of_house {
pub struct Breakfast {
pub x: String,//公有
y: String,//私有
}
}
pub enum
rust
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
5)use
关键字
- 可以使用
use
关键字将路径导入到作用域内 - 仍然遵守私有性规则
rust
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;//绝对路径
use front_of_house::hosting; //相对路径
//相当于 在这里定义了
pub mod hosting {
pub fn add_to_waitlist() {}
}
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
-
函数:将函数的父级模块引入作用域是常用做法
-
下面这种做法可以,但并不是习惯方式。
rust
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting::add_to_waitlist;
pub fn eat_at_restaurant() {
add_to_waitlist();
}
struct
,enum
,其他:指定完整路径(指定到本身)
rust
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();//直接指定到方法
map.insert(1, 2);
}
- 有一种情况两个不同的类,下面有同名的方法,我们不能指定本身,要加上父级路径
rust
use std::fmt;
use std::io;
fn f1() -> fmt::Result {}//会报错因为没有返回值
fn f2() -> io::Result {}//会报错
fn main() {}
6)as
我们有另外一种做法as
- as关键字可以为引入的路径指定本地的别名
rust
use std::fmt::Result;
use std::io::Result as IoResult;
fn f1() -> Result {}
fn f2() -> IoResult {}
fn main() {}
使用 pub use
重新导出名称
- 使用
use
将路径(名称)导入到作用域内后,该名称在此作用域内是私有的 - 可以将条目引入作用域
- 该条目可以被外部代码引入到它们的作用域
rust
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
意思就是,use
引入的模块,同一个文件是公有的,但是别的文件访问是私有的,解决这个问题只需要在use
前面加一个pub
就可以了
现在eat_at_restaurant
函数可以在其作用域中调用 hosting::add_to_waitlist
,外部代码也可以使用这个路径。
rust
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;//像这样
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
7)使用外部包
- Cargo.toml文件添加依赖的包
rust
[dependencies]
rand = "0.5.5"
- use 将特定条目引入作用域
- 标准库(std)也被当作外部包,但是不需要修改dependencies来包含它
9)使用嵌套路径清理大量的 use 语句
-
如果使用同一个包或模块下的多个条目
-
可以使用嵌套路径,在同一行内将上述条目进行引入
-
路径相同的部分 : : { 路径差异的部分 }
rust
use std::cmp::Ordering;
use std::io;
变为
rust
use std::{cmp::Ordering, io};
特殊情况:
rust
use std::io;
use std::io::Write;
变为
rust
use std::io::{self, Write};
10)通配符
我么可以使用 * 把路径中所有的公共条目都引入到作用域
把这个路径下的所有都引入了
rust
use std::collections::*;
谨慎使用
- 应用场景:
- 测试:将所有被测试代码引入tests模块
- 有时被用于预导入(prelude)模块
11)将模块内容移动到其他文件
- 模块定义时,如果模块名后边是 " ; " ,而不是代码块
- Rust 会从与模块同名的文件中加载内容
- 模块树的结构不会变化
两层分离
初始内容( lib.rs文件 )
rust
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
新建front_of_house.rs
文件
在lib.rs
文件中
rust
mod front_of_house;//从front_of_house文件引入
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
在front_of_house.rs
文件中
rust
pub mod hosting {
pub fn add_to_waitlist() {}
}
三层分离
如果想把,hosting 里面的内容再次独立出来
新建一个 front_of_house 的文件 ,里面写上hosting.rs

hosting.rs
内容
rust
pub fn add_to_waitlist() {}
front_of_house
内容
rust
pub mod hosting;
lib.rs
内容
rust
mod front_of_house;
use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
十一、常用的集合
1)Vector
rust
fn main() {
let v: Vec<i32> = Vec::new();
}
- 使用初始值创建
Vet<T>
,使用 vec! 宏
rust
fn main() {
let v = vec![1,2,3];
}
更新Vector
- 向 Vector 添加元素,使用 push 方法
rust
fn main() {
let mut v = Vec::new();
}
先写一行,你会发现这次我没有写里面的类型,他现在是会报错的,因为rust无法推断vector的类型
我们用push
rust
fn main() {
let mut v = Vec::new();
v.push(1);
v.push(2);
}
你发现不报错了,因为vector里有数据了,rust可以推断类型了
删除Vector
- 与任何其他的 struct 一样,当 Vector 离开作用域后
- 它就被清理掉了
- 它所有的元素也被清理掉了
rust
fn main() {
let v = vec![1,2,3];
}//到这里就自动被清理了
但是如果涉及到对 vector 里面的元素有引用的话,就会变复杂
读取 Vector 的元素
- 两种方式可以引用 Vector 里的值
- 索引
- get 方法
rust
fn main() {
let v = vec![1, 2, 3, 4, 5];
let third = v[2];
println!("The third element is {}", third);
match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),
}
}
输出:
rust
The third element is 3
The third element is 3
如果我们超出了索引
rust
fn main() {
let v = vec![1, 2, 3, 4, 5];
let third = v[100];//这里程序会panic恐慌
println!("The third element is {}", third);
match v.get(100) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element."),//这里会输出None的值
}
}
所以如果你想超出索引终止程序的话,就用索引的方式,如果不想中止就用get的方式
所有权规则
所有权规则在vector中也是适用的,不能在同一作用域内同时拥有可变和不可变引用
rust
fn main() {
let mut v = vec![1,2,3,4,5];
let first = &v[0];//不可变的借用
v.push(6);//可变的借用
println!("first is {}",first);//不可变的借用
}
v.push(6)
会报错,因为改变了v是可变的借用,而前面已经用了不可变的借用,违反了规则
遍历Vector中的值
- for 循环
rust
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v{
*i += 50;
}
for i in v{
println!("{}",i);
}
}
输出
rust
150
82
107
2)Vector + Enum 的例子
vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例。幸运的是,枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举。
rust
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
Rust 在编译时就必须准确的知道 vector 中类型的原因在于它需要知道储存每个元素到底需要多少内存。
3)String
-
Rust 的核心语言层面 ,只有一个字符串类型:字符串切片 str(或&str)
-
字符串切片:对存储在其他地方、UTF-8编码的字符串的引用
-
字符串字面值:存储在二进制文件中,也是字符串切片
-
String 类型:
-
来自 标准库 而不是 核心语言
-
可增长,可修改,可拥有
-
UTF-8 编码
通常说的字符串就是指的 String 和 &str
创建一个新的字符串(String)
String::new()
函数
rust
fn main() {
let mut s = String::new();//std::string::String
}
- 使用初始值来创建String
1、这新建了一个叫做 s
的空的字符串,接着我们可以向其中装载数据。可以使用 to_string
方法,它能用于任何实现了 Display
trait 的类型,字符串字面值也实现了它。
rust
fn main() {
let data = "initial contents";//&str 类型
let s = data.to_string();//std::string::String
let s1 = "initial contents".to_string();//std::string::String
}
2、直接使用String::from()
rust
fn main() {
let s = String::from("AAA");//std::string::String
}
更新String
push_str()
方法:
把一个字符串切片附加到 String
rust
fn main() {
let mut a = String::from("AAA");
let b = String::from("BBB");
a.push_str(&b);
println!("{}",a);
}输出AAABBB
push_str(这里用的是引用的切片)
,所以 b 还能继续使用
push()
方法:
把单个字符附加到String
rust
fn main() {
let mut a = String::from("AAA");
a.push('B');
}
+
拼接字符串
rust
fn main() {
let s1 = String::from("Hello, ");
let s2 = String::from("World!");
let s3 = s1 + &s2;
println!("{}", s3);//Hello, World
println!("{}", s1);//报错,
println!("{}", s2);//可以使用
}
字符串 s3
将会包含 Hello, world!
。s1
在相加后不再有效的原因,和使用 s2
的引用的原因,与使用 +
运算符时调用的函数签名有关。+
运算符使用了 add
函数,这个函数签名看起来像这样:
fn add(self, s: &str) -> String {
第一个参数self
,直接获取所有权了,然后销毁了本来的s1
,这就是为什么再用s1
会报错的原因
那为什么,第二个参数用的是 &str
,你传了一个&String
(+
号后面的&s2
是 &String
)编译还通过了呢?
因为&String
可以被 强转 (coerced )成 &str
。
当add
函数被调用时,Rust 使用了一个被称为 解引用强制多态 (deref coercion)的技术
format!:
连接多个字符串
如果用 +
连接多个显得很笨重
rust
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;
println!("{}", s)
}//输出tic-tac-toe
这时候我们使用 format!
这个宏
rust
fn main() {
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{}-{}-{}", s1, s2, s3);
println!("{}", s)
}//输出tic-tac-toe
对String按索引的形式进行访问
- 按索引语法访问 String 的某部分,会报错
rust
let s1 = String::from("hello");
let h = s1[0];
Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?
rust
fn main() {
let len1 = String::from("Hola").len();
let len2 = String::from("Здравствуйте").len();
println!("{}", len1);
println!("{}", len2);
}
输出:
rust
4
24
String
是一个 Vec<u8>
的封装。
"Hola" 的 Vec
的长度是四个字节:这里每一个字母的 UTF-8 编码都占用一个字节。
"Здравствуйте"是中如果你要返回З
你需要返回两个字节,那么你返回哪一个呢?
为了避免返回意外的值并造成不能立刻发现的 bug,Rust 根本不会编译这些代码,并在开发过程中及早杜绝了误会的发生。
还有一个原因是,索引操作预期总是需要常数时间 (O(1))。但是对于 String
不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。
字节、标量和字形簇
- Rust 有三种看待字符串的方式:
- 字节
- 标量值
- 字形簇(最接近所谓的"字母")
循环输出字节:
rust
fn main() {
let w = "नमस्ते";
for b in w.bytes(){
println!("{}",b)
}
}
输出:
rust
224
164
168
224
164
174
224
164
184
224
165
141
224
164
164
224
165
135
循环输出标量值
rust
fn main() {
let w = "नमस्ते";
for b in w.chars(){
println!("{}",b)
}
}
输出:
rust
न
म
स
्
त
े
那两个特殊的需要结合字符才表示意义,
字形簇才是所谓的四个字符"न म स त"
有需要可以去https://crates.io/这里找
切割String
- 可以使用【】和 一个范围 来创建字符串的切片
请看前面的第七章:切片
4)HashMap
-
键值对的形式存储数据,一个 Key 对应一个 Value
-
Hash 函数:决定如何在内存中存放 K 和 V
-
适用场景:通过 K(任何类型)来寻找数据,而不是通过索引
-
HashMap 是同构的,所有的 K 是同一类型,所有的 V 是同一类型
创建 HashMap
- 创建空的 HashMap:new()函数
- 添加数据:insert()方法
- 输出值:get()和 unwrap()方法
rust
use std::collections::HashMap;
fn main() {
let mut scores1: HashMap<String, i32> = HashMap::new(); //要么是这种声明HashMap内部数据类型
let mut scores2 = HashMap::new(); //要么是这种不声明数据类型,向其中添加数据
scores2.insert(String::from("分数"), 10);
println!("{}", scores2.get("分数").unwrap());
} //因为rust需要推断HashMap内部类型
HashMap的循环输出
rust
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert("color", "red");
map.insert("size", "10 m^2");
for p in map.iter() {
println!("{:?}", p);
}
}
运行结果:
text
("color", "red")
("size", "10 m^2")
另一种创建HashMap的方式
- 在元素类型为 Tuple 的 Vector 上使用 collect 方法,可以组建一个 HashMap:
- 要求 Tuple 要有两个值:一个作为K ,一个作为V
- collect 方法可以把数据整合成很多集合类型,包括 HashMap
- 返回值需要显式的指明类型
.iter()
返回遍历器,使用zip()
语法,就可以创建一个元组的数组,再用collect()
就能创建一个HashMap
rust
use std::collections::HashMap;
fn main() {
let teams = vec![String::from("Blue"), String::from("Yellow")];
let intial_scores = vec![10, 50];
let scores: HashMap<_, _> = teams.iter().zip(intial_scores.iter()).collect();
for p in scores.iter() {
println!("{:?}", p);
}
}
输出:
rust
("Blue", 10)
("Yellow", 50)
这里 HashMap<_, _>
类型注解是必要的,因为可能 collect
很多不同的数据结构,而除非显式指定否则 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 HashMap
所包含的类型。
HashMap的所有权
- 对于实现了Copy trait 的类型(例如 i32),值会被复制到 HashMap中
- 对于拥有所有权的值(例如 String),值会被移动,所有权会转移给HashMap
rust
use std::collections::HashMap;
fn main() {
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// 这里 field_name 和 field_value 不再有效,报错
// println!("{}: {}", field_name, field_value);
}
- 如果将值的引用插入到HashMap,值本身不会移动
- 在HashMapy有效期内,被引用的值必须保持有效
rust
use std::collections::HashMap;
fn main() {
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(&field_name, &field_value);
println!("{} : {}", field_name, field_value);
}
访问HashMap中的值
- get 方法
- 参数:K
- 返回:Option<&V>
rust
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score = scores.get(&team_name);
match score {
Some(s) => println!("{}", s),
None => println!("team not exist"),
}
}
输出:
rust
10
遍历HashMap
- for 循环
rust
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
for (k, v) in &scores {
println!("{} : {}", k, v);
}
}
输出:
rust
Blue : 10
Yellow : 50
更新HashMap
-
HashMap 大小可变
-
每一个 K 同时只能对应一个 V
-
更新 HashMap 中的数据
-
K 已经存在,对应一个 V
- 替换现有的 V
- 保留现有的 V,忽略新的 V(在 K 不对应任何 V 的情况下,才插入V)
- 合并现有的 V 和新的 V(基于现有 V 来更新 V)
-
K 不存在
- 添加一对 K,V
替换现有的V
- 如果向 HashMap 插入一对 K V,然后再插入同样的 K,但是不同的 V,那么原来的 V 会被替换掉
rust
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 50);
println!("{:?}", scores);
}
输出:
rust
{"Blue": 50}
在 K 不对应任何 V 的情况下,才插入V
entry
方法:检查指定的 K 是否对应一个 V- 参数为 K
- 返回 enum Entry:代表这个值是否存在
or_insert()
- 返回:
- 如果 K 存在,返回到对应的 V 的一个可变引用
- 如果 K 不存在,将参数方法作为 K 的新值插进去,返回到这个值的可变引用
rust
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{:?}", scores);
}
输出:
rust
{"Yellow": 50, "Blue": 10}
scores.entry(String::from("Yellow"))
的返回值是Entry(VacantEntry("Yellow"))
表示,HashMap中没有这个 K
scores.entry(String::from("Blue"))
的返回值是Entry(OccupiedEntry { key: "Blue", value: 10, .. })
,HashMap中有这个 K
然后使用了or_insert()
方法,这个方法里面有match
匹配,VacantEntry
表示没有 K ,就会Insert
,OccupiedEntry
表示有这个 K 就不会进行插入
基于现有 V 来更新 V
word 表示每一个单词,如果没有键插入数据0,然后自增1,
如果有这个键,就不插入数据0,直接自增1
rust
use std::collections::HashMap;
fn main() {
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
*count += 1;
}
println!("{:?}", map);
}
输出:
rust
{"wonderful": 1, "world": 2, "hello": 1}
哈希函数
HashMap
默认使用一种 "密码学安全的"("cryptographically strong" )1 哈希函数,它可以抵抗拒绝服务(Denial of Service, DoS)攻击。然而这并不是可用的最快的算法,不过为了更高的安全性值得付出一些性能的代价。如果性能监测显示此哈希函数非常慢,以致于你无法接受,你可以指定一个不同的 hasher 来切换为其它函数。hasher 是一个实现了 BuildHasher
trait 的类型。第十章会讨论 trait 和如何实现它们。你并不需要从头开始实现你自己的 hasher;crates.io 有其他人分享的实现了许多常用哈希算法的 hasher 的库。
十二、panic!
1)不可恢复的错误与 panic!
Rust 错误处理概述
-
Rust 的可靠性:错误处理
-
大部分情况下:在编译时提示错误并处理
-
错误分类:
-
可恢复
- 例如文件未找到,可再次尝试
-
不可恢复
- bug,例如访问的索引超出范围
-
Rust 没有类似异常的机制
-
可恢复错误:Result
-
不可恢复错误:panic! 宏
不可处理错误与 panic!
- 当 panic!宏执行(默认情况下):
- 你的程序会打印一个错误信息
- 展开(unwind)、清理调用栈(Stack)
- 退出程序
为应对 panic,展开或中止(abort)调用栈
-
默认情况下,当 panic 发生:
-
程序展开调用栈(工作量大)
- Rust 沿着调用栈往回走
- 清理每个遇到的函数中的数据
-
或立即中止调用栈:
-
不进行清理,直接停止程序
-
内存需要OS(操作系统)进行清理
-
如果你需要项目的最终二进制文件越小越好
-
panic 时通过在 Cargo.toml 的
[profile]
部分增加panic = 'abort'
,可以由展开切换为终止。 -
例如,如果你想要在release模式(生产环境下)中 panic 时直接终止:

一个小例子
rust
fn main() {
panic!("crash and burn")
}
输出:
显示了 panic 提供的信息并指明了源码中 panic 出现的位置:src/main.rs:2:5 表明这是 src/main.rs 文件的第二行第五个字符。

在这个例子中,被指明的那一行是我们代码的一部分,而且查看这一行的话就会发现 panic!
宏的调用。
在其他情况下,错误信息报告的文件名和行号可能指向别人代码中的 panic!
宏调用,而不是我们代码中最终导致 panic!
的那一行。我们可以使用 panic!
被调用的函数的 backtrace 来寻找代码中出问题的地方。下面我们会详细介绍 backtrace 是什么。
使用 panic!
的 backtrace
让我们来看看另一个因为我们代码中的 bug 引起的别的库中 panic!
的例子,而不是直接的宏调用。
尝试通过索引访问 vector 中元素的例子:
rust
fn main() {
let v = vec![1, 2, 3];
v[99];
}
输出:

提示说,设置RUST_BACKTRACE=1
,可以看到回溯信息
我们再次cargo run
,6 就是我们的代码文件,6 的上面就是 6 所调用的代码,6 的下面就是调用了 6 的代码

调试信息
带有调试信息的是cargo run
所以说默认就带有调试信息了
不带有调试信息的是cargo run --release
2)Result 枚举与可恢复的错误
Result 枚举
-
rust enum Result<T, E> { Ok(T), Err(E), }
-
T:操作成功情况下,Ok 变体里返回的数据的类型
-
E:操作失败情况下,Err 变体里返回的错误的类型
-
这个
f
就是Result
类型,成功返回File
,失败返回Error

处理 Result 的一种方式:match 表达式
- 和 Option 枚举一样,Result 及其变体也是由 prelude(预导入模块)带入作用域
rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let x = match f {
Ok(file) => file,
Err(error) => {
panic!("Error opening file {:?}", error)
}
};
}
输出:
没找到文件

匹配不同的错误
打开文件有两种情况
-
Ok 成功
-
Err 打开文件失败
match
匹配 -
没找文件,
match
匹配 -
那么我们就创建一个这个文件,也有两种情况
match
匹配 -
创建成功
-
创建失败
panic!
-
其他的情况导致文件打开失败
panic!
rust
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f1 = match f {
Ok(file) => file,
Err(error) => match error.kind() {//匹配io操作可能引起的不同错误
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {:?}", e),
},
other_error => panic!("Problem opening the file: {:?}", other_error),
},
};
}
我们运行一下,会在项目下生成一个 hello.txt
的文件
输出:

我们用更简单的方式来实现match
表达式
rust
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {:?}", error);
})
} else {
panic!("Problem opening the file: {:?}", error);
}
});
}
unwrap
- unwrap:match 表达式的一个快捷方法
- 打开文件,如果成功打开,返回文件,打开失败,返回异常
rust
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let test = match f {
Ok(file) => file,
Err(error) => {
panic!("Error opening file {:?}", error)
}
};
}
- 用
unwrap
的方式可以简写为一行 - 在读取的后面增加
unwrap
函数。如果文件存在,则直接返回 result 里面的值,也就是T ;如果文件不存在,则调用 panic! 宏,中止程序 。
rust
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let test2 = File::open("hello.txt").unwrap();
}
输出这样的报错信息,这个报错信息我们无法自定义,这也是unwrap
的缺点
rust
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "系统找不到指定的文件。" }', src\main.rs:5:41
expect
Rust 给我们提供了 expect
,它的功能和 unwrap
类似,但是它可以在其基础上指定错误信息
rust
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let test2 = File::open("hello.txt").expect("打开文件出错啦!!");
}
输出:
rust
thread 'main' panicked at '打开文件出错啦!!: Os { code: 2, kind: NotFound, message: "系统找不到指定的
文件。" }', src\main.rs:5:41
传播错误(包含Result作为函数返回值写法)
之前所讲的是接收到错误的处理方式,但是如果我们自己编写一个函数在遇到错误时想传递出去怎么办呢?
rust
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => return Ok(s),
Err(e) => return Err(e),
}
}
fn main() {
match read_username_from_file() {
Ok(t) => println!("{}", t),
Err(e) => panic!("{}", e),
}
}
代码说明:
第5
行:read_username_from_file()
函数的目的是在一个文件
中读出用户名
,返回一个Result
,操作成功返回String
,失败返回io::Error
第10
行:如果打开文件失败,Err(e)
会作为返回值,符合Result<String,io::String>
错误返回io::String
的返回值类型
第15-18
行: f
代表打开的文件,从中读取字符串,赋给 s
,成功返回读取的字符串给 Result<String,io::String
>中的String
的返回值类型,失败返回io::String
的返回值类型,如果不写两个return
也可以
rust
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
因为最后一个match
表达式,不用写return
就可以表示为函数的返回值
?
运算符
?
运算符:传播错误的一种快捷方式- 如果
Result
是Ok
:Ok
中的值就是表达式的结果,然后继续执行程序 - 如果
Result
是Err
:Err
就作为整个函数的返回值返回
rust
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
fn main() {
match read_username_from_file() {
Ok(t) => println!("{}", t),
Err(e) => panic!("{}", e),
}
}
代码说明:
第6
行:打开文件,失败返回错误
第10
行:读取文件中的字符串给s
,失败返回错误
第12
行:返回成功的字符串s
?
与from
函数
-
from
函数来自于 标准库std::convert::From
的这个Trait 上的from
函数 -
from
函数的作用就是错误之间的转换,将一个错误类型转换为另一个错误类型 -
被
?
所应用的错误,会隐式的被from
函数处理 -
它所接收的错误类型会被转化为当前函数返回类型所定义的错误类型
-
比如说上方代码的第
6
行,如果发生错误,File::open
的错误类型不是io::Error
,而这行后面有?
,那么就会转化为io::Error
类型 -
但并不是任意两个错误类型都可以进行相互转化
-
如果想要,错误类型A(EA)转化为 错误类型B (EB),那么就需要 EA 实现了一个 返回值类型是 EB的
from
函数 -
用于:针对不同错误原因,返回同一种错误类型
?
运算符的链式调用
rust
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
fn main() {
match read_username_from_file() {
Ok(t) => println!("{}", t),
Err(e) => panic!("{}", e),
}
}
代码说明:
第8
行:文件打开,和读取文件内字符串都成功时,程序继续执行,函数返回Ok(s)
,如果哪个操作失败了,就会把相应的报错作为函数的返回值返回
?
运算符只能用于返回Result
的函数
rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt")?;
}
运行报错为:?
运算符只能用于函数返回结果是Result
、Option
或者是实现了FromResidual
的类型
?
运算符与main函数
- main 函数返回类型是
()
- main 函数的返回类型也可以是:
Result<T,E>
rust
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
代码说明:
第4行:main 函数不发生错误返回()
,发生错误返回Box<dyn Error>
,Box<dyn Error>
是 trait
对象,可以简单的理解为任意类型的错误
3)何时使用panic!
总体原则
-
在定义一个可能失败的函数时,优先考虑返回 Result
-
否则就
panic!
()
使用panic!
的场景
- 演示某些概念(写伪代码的时候)
- 原型代码(直接
panic!
程序会中止,代码在测试环境可以用,生产环境尽量不要有panic!
,可以用panic!
作为一个明显的标记) - 测试代码(
panic!
就代表测试没通过)
十三、泛型,Trait,生命周期
1)提交函数消除重复代码(包含引用与解引用)
初始代码
rust
fn main() {
let number_list = vec![34, 50, 21, 100, 44];
let mut largest = number_list[0];
for number in number_list {
if number > largest {
largest = number;
}
}
println!("{}", largest);
let number_list = vec![340, 500, 210, 1000, 440];
let mut largest = number_list[0];
for number in number_list {
if number > largest {
largest = number;
}
}
println!("{}", largest)
}
代码说明:
第2
行:声明一个Vector
第3
行:将 34
赋给 largest
第4-8
行:循环Vector
,拿出每一个值,赋给 number
,实现number_list
中最大值赋给largest
的功能
第11-18
行:重复代码
简化代码
rust
fn largest(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 21, 100, 44];
let result = largest(&number_list);
println!("{}", result);
let number_list = vec![340, 500, 210, 1000, 440];
let result = largest(&number_list);
println!("{}", result);
}
代码说明:
第1
行:largest(list: &[i32])
中largest
方法传参是i32
类型的切片
第3-7
行:item
是list
中的每个值,item
默认是&i32
类型,加一个&
变为&item
,后续的item
就变为了i32
类型,就能和largest
这个i32
类型的进行比较和赋值了
如果把代码变为这样
rust
fn largest(list: &[i32]) -> i32 {
let mut largest = list[0];
for item in list {
if item > largest {//报错
largest = item;//报错
}
}
largest
}
因为item
是&i32
类型,largest
是i32
类型,无法进行比较和赋值
我们可以这样写
rust
fn largest(list: &[i32]) -> i32 {
let mut largest = list[0];
for item in list {
if item > &largest {
largest = *item
}
}
largest
}
或者是这样,用*
进行解引用
rust
fn largest(list: &[i32]) -> i32 {
let mut largest = list[0];
for item in list {
if *item > largest {
largest = *item
}
}
largest
}
2)泛型
- 泛型:提高代码的复用能力
- 处理重复代码的问题
- 泛型是具体类型或其他属性的抽象代替
- 你编写的代码不是最终的代码,而是一种模板 ,里面有一些 "占位符"
- 编译器在编译时 将"占位符" 替换为具体的类型
- 例如:
fn largest<T>(list: &[T]) -> {...}
里面的T
就是"占位符"
我们之前写过这样的代码:遍历出Vector
中的最大值输出出来
rust
fn largest(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 21, 100, 44];
let result = largest(&number_list);
println!("{}", result);
let number_list = vec![340, 500, 210, 1000, 440];
let result = largest(&number_list);
println!("{}", result);
}
输出:
rust
100
1000
现在我们在第16
行把Vector
变为字符的集合
第17
行会报错
rust
fn largest(list: &[i32]) -> i32 {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 21, 100, 44];
let result = largest(&number_list);
println!("{}", result);
let number_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&number_list);
println!("{}", result);
}
我们可以用泛型来解决,写成这样,在代码第1
行,声明是个泛型的函数,传参和返回值都是泛型的,在第4
行会报错
rust
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 21, 100, 44];
let result = largest(&number_list);
println!("{}", result);
let number_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&number_list);
println!("{}", result);
}
输出:

不是所有的类型T
都能比较大小,要实现 std::cmp::PartialOrd
这个 trait (接口)才行
但如果在代码的第1
行这样写fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
,又会有其他的报错,我们后面会解决这个问题
在Struct
(结构体)中定义的泛型
rust
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10 };
let float = Point { x: 5.0, y: 10.0 };
}
那么如果我想要结构体中两个不同类型的参数呢,比如一个i32
,一个f64
,像这样,在代码第6
行 y:10.0
会报错expected integer 他期望是一个整数
rust
struct Point<T> {
x: T,
y: T,
}
fn main() {
let integer = Point { x: 5, y: 10.0 };
}
我们这样就能够解决了
rust
struct Point<T, U> {
x: T,
y: U,
}
fn main() {
let integer = Point { x: 5, y: 10.0 };
}
在Enum(枚举)中使用泛型
类似于结构体,枚举也可以在其成员中存放泛型数据类型。第六章我们使用过了标准库提供的 Option<T>
枚举,让我们再看看:
rust
enum Option<T> {
Some(T),
None,
}
现在这个定义看起来就更容易理解了。如你所见 Option<T>
是一个拥有泛型 T
的枚举,它有两个成员:Some
,它存放了一个类型 T
的值,和不存在任何值的None
。通过 Option<T>
枚举可以表达有一个可能的值的抽象概念,同时因为 Option<T>
是泛型的,无论这个可能的值是什么类型都可以使用这个抽象。
枚举也可以拥有多个泛型类型
rust
enum Result<T, E> {
Ok(T),
Err(E),
}
Result
枚举有两个泛型类型,T
和 E
。Result
有两个成员:Ok
,它存放一个类型 T
的值,而 Err
则存放一个类型 E
的值。这个定义使得 Result
枚举能很方便的表达任何可能成功(返回 T
类型的值)也可能失败(返回 E
类型的值)的操作。回忆一下打开一个文件的场景:当文件被成功打开 T
被放入了 std::fs::File
类型而当打开文件出现问题时 E
被放入了 std::io::Error
类型。
在方法中使用泛型
为 struct
或 enum
实现方法的时候,可以使用泛型
rust
struct Point<T> {
x: T,
y: T,
}
impl<T> Point<T> {
fn ret(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5, y: 10 };
println!("p.x = {}", p.ret());
}
代码说明:
第6
行:impl<T>
表示实现的方法用到了泛型,Point<T>
表示传的参数是Point
类型的携带的数据是T
泛型
第7
行:方法ret
,传了&self
他自己就是Point
,其实是传的p
,返回了一个泛型T
第15
行:输出方法,p
的ret
方法,传入p
给&self
,返回p
的x
也就是&self.x
如果要针对具体的类型
在代码第6
行,不需要写impl<T> Point<T> {...}
要直接写成这样impl Point<f64> {...}
rust
struct Point<T> {
x: T,
y: T,
}
impl Point<f64> {
fn ret(&self) -> &T {
&self.x
}
}
fn main() {
let p = Point { x: 5.1, y: 10.1 };
println!("p.x = {}", p.ret());
}
struct
里的泛型类型参数如果和方法的泛型类型参数不同
rust
struct Point<T, U> {
x: T,
y: U,
}
impl<T, U> Point<T, U> {
fn mixup<V, W>(self, other: Point<V, W>) -> Point<T, W> {
Point {
x: self.x,
y: other.y,
}
}
}
fn main() {
let p1 = Point { x: 5, y: 10.4 };
let p2 = Point { x: "Hello", y: 'c' };
let p3 = p1.mixup(p2);
println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
输出:
rust
p3.x = 5, p3.y = c
代码说明:
第16
行:p1
是Point<T, U>
,T
是 i32
, U
是f64
第17
行:p1
是Point<T, U>
,T
是 字符串切片&str
, U
是char
第19
行:p1.mixup(p2)
,用到的这个mixup()
方法,直接去看代码第7
行
第7
行:
fn mixup<V, W>
声明了会用<V, W>
泛型
(self, other: Point<V, W>)`传递的第一个参数是`self`,其实就是`p1`,第二个参数是`other: Point<V, W>`,传递的是`p2`,特意说明了`Point<V, W>
不能写Point<T, U>
,因为T
U
已经分别代表i32
和f64
了,就要用两个新的泛型来代表&str
和char
也就是V
和W
-> Point<T, W>`返回值是一个`T`一个`W`就是一个`i32`一个`char
第8-11
行:返回Point
x: self.x
,x值是self
的x
,就是p1
的x
,也就是5
,i32
类型
y: other.y
,y值是other
的y,就是p2
的y
,也就是c
,char
类型
泛型代码的性能
- 使用泛型的代码和使用具体类型的代码运行速度是一样的
- Rust在编译的时候,会进行单态化(monomorphization),就是在编译时就将泛型替换为具体类型的过程
下面的代码在编译时会怎么样呢?
rust
let integer = Some(5);
let float = Some(5.0);
当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给 Option<T>
的值并发现有两种 Option<T>
:一个对应 i32
另一个对应 f64
。为此,它会将泛型定义 Option<T>
展开为 Option_i32
和 Option_f64
,接着将泛型定义替换为这两个具体的定义。
编译器生成的单态化版本的代码看起来像这样
rust
enum Option_i32 {
Some(i32),
None,
}
enum Option_f64 {
Some(f64),
None,
}
fn main() {
let integer = Option_i32::Some(5);
let float = Option_f64::Some(5.0);
}
这意味着在使用泛型时没有运行时开销,在编译期就已经产生具体定义的方法了
3)Trait(上)
- Trait 告诉 Rust 编译器:某个特定类型拥有可能与其他类型共享的功能
- Trait:抽象的定义共享行为
- Trait bounds(约束):泛型类型参数指定为实现了特定行为的类型
- Trait 类似于其他语言中的常被称为 接口 (interfaces)的功能,虽然有一些不同。
定义一个Trait
- Trait 的定义:把方法签名放在一起,来定义实现某种目的所必需的一组行为
- 关键字:trait
- 只有方法签名,没有具体实现
- trait 可以有多个方法:每个方法签名占一行,以 ; 结尾
- 实现该 trait 的类型必须提供具体的方法实现

main.rs
文件
rust
use demo::Summary;
use demo::Tweet;
fn main() {
let tweet = Tweet {
username: String::from("用户名"),
content: String::from("显示content"),
reply: false,
retweet: false,
};
println!("我们来用一下tweet: {}", tweet.summarize());
}
代码说明:
第1-2
行:导入了demo
下的Summary
和Tweet
第4-9
行:声明tweet
这个struct
(从lib.rs
引用的demo::Tweet
用于这里)
第10
行:tweet
用了summarize()
方法(从lib.rs
引用的use demo::Summary
用于这里)
lib.rs
文件
rust
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct Tweet {
pub username: String,
pub content: String,
pub reply: bool,
pub retweet: bool,
}
impl Summary for Tweet {
fn summarize(&self) -> String {
format!("{}, {}", self.username, self.content)
}
}
代码说明:
第1-3
行:声明一个trait
(接口)Summary
,有一个summarize
方法,
第12
行:用Summary
这个trait
实现一个叫Tweet
的struct
结构体
第13
行:&self
就是main.js
中的tweet
输出:
rust
我们来用一下tweet: 用户名, 显示content
总结:其实就是lib.rs
中Tweet
这个struct
实现了Summary
这个trait
,然后在main.js
中声明一个tweet
的实例,可以用Summary
里面的方法
实现trait的约束
-
当某一个类型实现了某个
trait
的前提条件是:这个类型或者是这个trait
在本地的crate
里定义了 -
要么是类型(例如struct)在本地定义,我们去实现外部的trait,要么是trait是在本地定义的,我们是用外部的类型(struct)去实现本地的trait
-
如果两个都是外部的,外部的类型去实现外部的trait是不可以的
默认实现
这就是与接口不同的地方,trait可以自己定义一个方法作为默认方法
rust
trait Descriptive {
fn describe(&self) -> String {
String::from("[Object]")
}
}
struct Person {
name: String,
age: u8,
}
impl Descriptive for Person {
fn describe(&self) -> String {
format!("{} {}", self.name, self.age)
}
}
fn main() {
let cali = Person {
name: String::from("lizehui"),
age: 24,
};
println!("{}", cali.describe());
}
输出:
rust
lizehui 24
如果我们去掉代码第13-15
行,他就会实现默认方法
rust
trait Descriptive {
fn describe(&self) -> String {
String::from("[Object]")
}
}
struct Person {
name: String,
age: u8,
}
impl Descriptive for Person {}
fn main() {
let cali = Person {
name: String::from("lizehui"),
age: 24,
};
println!("输出:{}", cali.describe());
}
输出:
rust
输出:[Object]
如果我们在trait中嵌套使用方法呢,这样会报错
rust
trait Descriptive {
fn describe(&self) -> String;
fn new_describe(&self) -> String {
self.describe()
}
}
struct Person {
name: String,
age: u8,
}
impl Descriptive for Person {}
fn main() {
let cali = Person {
name: String::from("lizehui"),
age: 24,
};
println!("输出:{}", cali.new_describe());
}
代码说明:
第2
行:声明describe
方法
第3
行:new_describe
方法中用了describe
方法
第13
行:报错,Persion
实现了Descriptive
,报错的原因是new_describe
虽然是默认的已经实现的方法,但是里面包含了没有实现的方法describe
,要把describe
实现才能消除错误,像这样
rust
trait Descriptive {
fn describe(&self) -> String;
fn new_describe(&self) -> String {
self.describe()
}
}
struct Person {
name: String,
age: u8,
}
impl Descriptive for Person {
fn describe(&self) -> String {
format!("111")
}
}
fn main() {
let cali = Person {
name: String::from("lizehui"),
age: 24,
};
println!("输出:{}", cali.new_describe());
}
输出:
rust
输出:111
4)Trait(下)
Trait作为参数
rust
trait Descriptive {
fn describe(&self) -> String;
}
struct Person {
name: String,
age: u8,
}
impl Descriptive for Person {
fn describe(&self) -> String {
format!("{},{}", &self.name, &self.age)
}
}
fn main() {
let cali = Person {
name: String::from("lizehui"),
age: 24,
};
fn output(object: impl Descriptive) -> String {
object.describe()
}
println!("输出:{}", output(cali));
}
输出:
rust
输出:lizehui,24
代码说明:
第1-3
行:声明一个trait
,里面有一个方法describe
,实现这个trait
就要实现describe
这个方法
第5-8
行:声明一个struct
,Person
第10-14
行:Person
这个struct
实现了Descriptive
这个trait
,当实例化一个Person
对象的时候,这个对象可以用describe
这个方法了
第17-20
行:实例化一个Person
对象叫做cali
第21-23
行 :声明一个output
方法,参数是impl Descriptive
表示任何实现了Descriptive
这个trait
的类型都能作为参数,返回了object.describe()
,表示返回传入参数object
调用describe()
这个方法的返回值
第24
行:输出
语法糖
rust
fn output(object: impl Descriptive) -> String {
object.describe()
}
可以等效的写为,这被称为trait bound
rust
fn output<T: Descriptive>(object: T) -> String {
object.describe()
}
传多个参数
如果传多个参数,在代码的第25
行,会很长
rust
trait Descriptive {
fn describe(&self) -> String;
}
struct Person {
name: String,
age: u8,
}
impl Descriptive for Person {
fn describe(&self) -> String {
format!("{},{}", &self.name, &self.age)
}
}
fn main() {
let cali = Person {
name: String::from("lizehui"),
age: 24,
};
let cali1 = Person {
name: String::from("lizehui1"),
age: 24,
};
fn output(object: impl Descriptive, object1: impl Descriptive) -> String {
format!("{},{}", object.describe(), object1.describe())
}
println!("输出:{}", output(cali, cali1));
}
我们用语法糖trait bound
的写法,会发现第25
行精简了很多
rust
trait Descriptive {
fn describe(&self) -> String;
}
struct Person {
name: String,
age: u8,
}
impl Descriptive for Person {
fn describe(&self) -> String {
format!("{},{}", &self.name, &self.age)
}
}
fn main() {
let cali = Person {
name: String::from("lizehui"),
age: 24,
};
let cali1 = Person {
name: String::from("lizehui1"),
age: 24,
};
fn output<T: Descriptive>(object: T, object1: T) -> String {
format!("{},{}", object.describe(), object1.describe())
}
println!("输出:{}", output(cali, cali1));
}
让传进来的参数,实现多个trait
用 +
来让参数实现多个trait
rust
trait Descriptive {
fn describe(&self) -> String;
}
trait Print {
fn print_function(&self) -> String;
}
struct Person {
name: String,
age: u8,
}
impl Descriptive for Person {
fn describe(&self) -> String {
format!("{}", &self.name)
}
}
impl Print for Person {
fn print_function(&self) -> String {
format!("{}", &self.age)
}
}
fn main() {
let cali = Person {
name: String::from("lizehui"),
age: 24,
};
fn output(object: impl Descriptive + Print) -> String {
format!("{}, {}", object.print_function(), object.describe())
}
println!("输出:{}", output(cali));
}
输出:
rust
输出:24, lizehui
代码说明:
第1-6
行:声明两个trait
第8-11
行:声明struct
第13-17
行:实现Descriptive
输出Person
类型的name
第19-23
行:实现Descriptive
输出Person
类型的age
第26-29
行:声明Person
类型的实例
第30
行: object
代表了,实现Descriptive + Print
两个trait的参数
**第31
行:**分别用实现了这两个trait
中的方法,也就是输出Person
类型的name
和age
语法糖trait bound
让参数实现多个trait
的写法
仅在第30
行改变
rust
trait Descriptive {
fn describe(&self) -> String;
}
trait Print {
fn print_function(&self) -> String;
}
struct Person {
name: String,
age: u8,
}
impl Descriptive for Person {
fn describe(&self) -> String {
format!("{}", &self.name)
}
}
impl Print for Person {
fn print_function(&self) -> String {
format!("{}", &self.age)
}
}
fn main() {
let cali = Person {
name: String::from("lizehui"),
age: 24,
};
fn output<T: Descriptive + Print>(object: T) -> String {
format!("{}, {}", object.print_function(), object.describe())
}
println!("输出:{}", output(cali));
}
输出:
rust
输出:24, lizehui
通过 where
简化 trait bound
你看这个方法,它的函数签名(签名就是形容这个方法的一些标签)fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32
,·这里面<T: Display + Clone, U: Clone + Debug>
表示有两个不同类型的参数,一个实现了Display
和Clone
,一个实现了Clone
和Debug
,很长难以阅读
rust
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {
//方法中的内容
}
简化写法:
rust
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,
U: Clone + Debug
{
//方法中的内容
}
用Trait
作为返回类型
rust
trait Descriptive {
fn describe(&self) -> Person;
}
#[derive(Debug)]
struct Person {
name: String,
age: u8,
}
impl Descriptive for Person {
fn describe(&self) -> Person {
Person {
name: String::from("李泽辉"),
age: 22,
}
}
}
fn output(object: impl Descriptive) -> impl Descriptive {
object.describe()
}
fn main() {
let cali = Person {
name: String::from("随便写"),
age: 00,
};
println!("{:?}", output(cali).describe());
}
输出:
rust
Person { name: "李泽辉", age: 22 }
代码说明:
第26
行:output(cali)
传入参数,看代码第18
行,传入实现了Desciptive
的类型,也就是传入Person
,用object
代表
在代码第10-17
行,Person
实现了Desciptive
,
第18
行-> impl Descriptive
要返回实现了Desciptive
的类型,用object
中describe()
方法,返回了Person
,因为Person
实现了Descriptive
,满足条件
第26
行output(cali).describe())
为什么不写成output(cali)
呢,感觉也是没错的,但是rust只看签名(也就是形容这个方法的标签,一般都是一整行),你返回的是impl Descriptive
,即便在output()
方法体中告诉了,是返回object.describe()
,Rust也不知道,所以你要用output(cali).describe()
,来显式的表示,你要用这个方法
特性做返回值的限制
只接受实现了该特性的对象做返回值且在同一个函数中所有可能的返回值类型必须完全一样。:
下面这个函数就是错误的,A
和B
都实现了Descriptive
,但是也是不行的
rust
fn some_function(bool bl) -> impl Descriptive {
if bl {
return A {};
} else {
return B {};
}
}
解决之前传参T无法用>
比较的问题
关于我们在第十三章,2)泛型中写了一个功能,使用了两个不同类型的Vector
,进行比较大小报错的问题,我们现在能解决了
rust
fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![34, 50, 21, 100, 44];
let result = largest(&number_list);
println!("{}", result);
let number_list = vec!['y', 'm', 'a', 'q'];
let result = largest(&number_list);
println!("{}", result);
}
代码说明:
第1
行:<T: PartialOrd + Copy>
实现PartialOrd
为了比较大小,然后会报错所以要实现两个PartialOrd + Copy
,Copy
是基本类型在Stack上的数据进行复制的操作
引用类型比较
那么我们想要让引用类型进行比较呢,这样就行了
rust
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
fn main() {
let number_list = vec![String::from("hello"), String::from("world")];
let result = largest(&number_list);
println!("{}", result);
}
使用 trait bound 有条件地实现方法
通过使用带有 trait bound 的泛型参数的 impl
块,可以有条件地只为那些实现了特定 trait 的类型实现方法。
rust
fn main() {
use std::fmt::Display;
#[derive(Debug)]
struct Pair<T> {
x: T,
y: T,
}
impl<T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self { x, y }
}
}
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self) {
if self.x >= self.y {
println!("The largest member is x = {}", self.x);
} else {
println!("The largest member is y = {}", self.y);
}
}
}
let x = Pair { x: 1, y: 2 };
println!("{:?}", x);
println!("{:?}", x.cmp_display());
}
输出:
rust
Pair { x: 1, y: 2 }
The largest member is y = 2
代码说明:
第10-14
行:所有的Pair
,无论传入里面的T
是什么参数都会有一个new
函数,在第26
行,只要实例化,就会实现这个new
函数
第16
行:只有传入的参数T
实现了Display
和PartialOrd
方法,才能使用cmp_display
方法,这个例子中传入的是u32
,本身是实现了这两个方法的
标准库中的例子
声明了一个to_string()
的trait

impl<T: fmt::Display + ?Sized> ToString for T {
是ToString
的实现,传入的参数T
只有满足实现fmt::Display
才可以使用to_string
方法

main.js
,3
实现了fmt::Display
所以拥有to_string
方法
rust
fn main() {
let s = 3.to_string();
}
十四、生命周期
- Rust的每个引用都有自己的生命周期
- 生命周期:让引用保持有效的作用域
- 大多数情况下:生命周期是隐式的、可被推断的
- 当引用的生命周期可能以不同的方式互相关联时:手动标注生命周期
1)避免悬垂引用
- 生命周期的主要目标:避免悬垂引用(dangling reference)
- 运行下面的代码
rust
fn main() {
let x;
{
let y = 4;
x = &y;
}
print!("{}", x);
}
代码说明:
第2
行:声明一个变量x
第3-6
行:声明一个y
值为4
,把y
的引用赋值给x
,在第5
行会报错
第7
行:输出x
输出:

报错原因:
borrowed value does not live long enough
借用的值活得时间不够长
- y dropped here while still borrowed
,y
走到这里花括号结束的时候,y
对应的内存已经被释放了
borrow later used here
,而在此之后我们又使用了x
,而x
指向的就是y
,Rust为了安全,任何基于x
的操作都是无法进行的
2)借用检查器(borrow checker)
- Rust是如何确定这段代码是不合法的呢?
- Rust编译器的借用检查器:它比较作用域来确保所有的借用都是合法有效的

说明:x
这个变量的生命周期被标记为'a
,y
这个变量的生命周期被标记为'b
,在编译时,Rust发现,x
拥有生命周期'a
,但是它引用了另一个拥有生命周期 'b
的对象,由于生命周期 'b
比生命周期 'a
要小,被引用的对象比它的引用者存在的时间短,程序被Rust拒绝编译。
我们来看一下没有产生悬垂引用且可以正确编译的例子

说明:x
的生命周期'a
,引用了y
的生命周期'b
,被引用的生命周期,长于引用的生命周期
3)函数中的泛型生命周期
这个程序很简单,传入两个字符串切片,哪个长,返回哪个
代码第9
行,第11
行,第13
行都报错
我们cargo run
一下

missing lifetime specifier
:缺少生命周期的标注
consider introducing a named lifetime parameter
:考虑引入命名的生命周期参数像下面那样
rust
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
报错解释:
要通过'a
这种方式,告诉借用检查器,传入的x
和y
跟返回的&str
生命周期是相同的,因为函数是不能知道它引用的参数到底是什么情况,说不定已经失效了呢,防止这种现象的发生
修改后的代码
rust
fn main() {
let string1 = "abcd";
let string2 = "xyz";
let result = longest(string1, string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
输出:
rust
The longest string is abcd
代码说明:
在下面4)生命周期标注
中会谈到
4)生命周期标注
- 生命周期的标注:描述了多个引用的生命周期的关系,但不影响生命周期,
- 我们并没有改变任何传入和返回的值的生命周期。而是指出任何不遵守这个协议的传入值都将被借用检查器拒绝。
语法
- 生命周期参数名:
- 以
'
为开头 - 通常全部小写而且很短
- 比如说经常使用的
'a
- 生命周期标注的位置
- 在引用
&
符号后面 - 使用空格将标注和引用类型分开
例子
&i32
一个引用&'a i32
带有显式生命周期的引用&'a mut i32
带有显式生命周期的可变引用
5)在函数签名中的生命周期标注
- 泛型生命周期参数声明在:函数名和参数列表之间的
<>
里 - 就像上面的例子那样
rust
fn main() {
let string1 = "abcd";
let string2 = "xyz";
let result = longest(string1, string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
代码第9
行:<'a>
表示longest
这个方法中会有用到'a
生命周期的地方
泛型生命周期 'a
的具体生命周期等同于 x
和 y
的生命周期中较小的那一个。因为我们用相同的生命周期参数 'a
标注了返回的引用值,所以返回的引用值就能保证在 x
和 y
中较短的那个生命周期结束之前保持有效。(当然在这个代码中传入的两个参数生命周期是相同的)
我们来试一下让传入参数x
和y
的生命周期不同会怎样
rust
fn main() {
let string1 = "abcd";
let result;
{
let string2 = "xyz";
result = longest(string1, string2);
}
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
代码说明:
string1
的生命周期是代码第2-9
行,string2
的生命周期是5-9
行,并不会报错,因为在代码第8
行用到result
的时候,string2
的生命周期并没有结束,因为string2
是&str
类型,你可以被想象成一个静态的代码,不会在代码第7
行就结束
而如果变为这样
在代码第5
行变为String::from("xyz")
在代码第6
行变为string2.as_str()
,as_str()
能把String
类型变为&str
类型,让参数符合方法条件
这样变一下,代码第6
行的string2
就会报错,因为string2
是String
类型它的生命周期在代码第4-7
行,也就是说,在代码第8
行,用到了result
,而result
的值来自与longest
这个方法的返回值,方法需要的参数string2
已经在第七行被销毁了,而我们之前说过,返回的引用值就能保证在参数中较短的那个生命周期结束之前保持有效 ,所以返回值result
的生命周期会和string2
相同,所以在代码第8
行用到result时候,它的生命周期已经和string2
一样结束了,自然用不到,报错会提示你让string2
的生命周期再长一些
rust
fn main() {
let string1 = "abcd";
let result;
{
let string2 = String::from("xyz");
result = longest(string1, string2.as_str());
}
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
6)深入理解生命周期
指定生命周期参数的方式依赖于函数所做的事情
rust
fn main() {
let string1 = "abcd";
let string2 = "xyz";
let result = longest(string1, string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
输出:
rust
The longest string is abcd
我们改一下代码第10-14
行,让返回值只返回一个参数,也就是y
rust
fn main() {
let string1 = "abcd";
let string2 = "xyz";
let result = longest(string1, string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
y
}
输出:
rust
The longest string is xyz
现在这个函数longest
返回的值生命周期就和y
有关,那我们也就可以把代码第9
行,关于指定x
的生命周期'a
删掉了
rust
fn main() {
let string1 = "abcd";
let string2 = "xyz";
let result = longest(string1, string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &str, y: &'a str) -> &'a str {
y
}
从函数返回引用不指向任何参数
- 从函数返回引用时,返回类型的生命周期参数需要与其中一个参数的生命周期匹配
- 如果返回的引用没有指向任何参数,那么它只能引用函数内创建的值
- 这很容易出现悬垂引用,指向的内存已经被清理掉了
rust
fn main() {
let string1 = "abcd";
let string2 = "xyz";
let result = longest(string1, string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
let ret = String::from("ret");
ret.as_str()
}
代码说明:
代码第11
行报错
这个longest
方法返回值没用到任何参数,它返回了ret
这个变量,ret
会在代码第12
行的时候drop
掉
在代码第5
行longest
这个方法的返回值赋值给了result
,而ret
内存已经被清理掉了,这就发生了悬垂引用
那我们要是就是想返回函数内的变量,不想返回方法的入参呢!?
简单,不返回引用,把所有权移交给函数的调用者result
就完了
rust
fn main() {
let string1 = "abcd";
let string2 = "xyz";
let result = longest(string1, string2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> String {
let ret = String::from("ret");
ret
}
7)Struct定义中的生命周期标注
- 我们前面学
struct
的时候,都是自持有类型(比如:i32
,String
) - 如果
struct
字段是引用类型,需要添加生命周期标注 - 看下面的例子
rust
struct ImportantExcerpt<'a> {
part: &'a str,
}
fn main() {
let novel = String::from("Call me Ishmael. Some years ago...");
let first_sentence = novel.split('.').next().expect("Could not find a '.'");
print!("{}", first_sentence);
let i = ImportantExcerpt {
part: first_sentence,
};
}
输出:
rust
Call me Ishmael
代码说明:
第1行:声明一个struct,因为里面有一个切片(引用、&str
,你叫啥都行),所以需要<'a>
和&'a str
来标记生命周期,这意味着这个struct
:ImportantExcerpt
一定要大于等于&str
的生命周期,因为不能出现悬垂指针的情况
第6-7
行:对string
一顿操作,first_sentence
是&str
类型
第8
行:输出Call me Ishmael
,把第一个.
之前的字符串截取下来
第9-11
行:写一个ImportantExcerpt
的实例
first_sentence
的生命周期是第8-12
行,在第10
行被ImportantExcerpt
使用时候,没有结束,不报错
8)生命周期的省略
我们知道这个件事:每个引用都有生命周期,而且需要为使用生命周期的函数或struct
做生命周期的标注
下面代码的说明,按Ctrl+F
输入first_word
,在第七章 重新温故一下,
rust
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
你看这个函数,没有标注任何生命周期,仍然能通过编译,在早期版本(pre-1.0)的 Rust 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样:
rust
fn first_word<'a>(s: &'a str) -> &'a str {
在编写了很多 Rust 代码后,Rust 团队发现在特定情况下 Rust 程序员们总是重复地编写一模一样的生命周期注解。这些场景是可预测的并且遵循几个明确的模式。接着 Rust 团队就把这些模式编码进了 Rust 编译器中,如此借用检查器在这些情况下就能推断出生命周期而不再强制程序员显式的增加注解。
这里我们提到一些 Rust 的历史是因为更多的明确的模式被合并和添加到编译器中是完全可能的,未来需要手动标注的生命周期会越来越少。
生命周期省略规则
- 在Rust引用分析中所编入的模式称为:生命周期省略规则
- 这些规则无需开发者来遵守
- 他们是一些特殊情况,由编译器来考虑
- 如果你的代码符合这些情况,那么就无需显式标注生命周期
- 生命周期省略规则不会提供完整的推断:
- 如果应用规则后,引用的生命周期仍然模糊不清------>编译错误
- 解决办法:添加生命周期标注,表明引用间的相互关系
输入、输出生命周期
- 生命周期在:
- 函数/方法的参数:输入生命周期
- 函数/方法的返回值:输出生命周期
生命周期省略的三个规则
-
编译器使用
3
个规则在没有显式标注生命周期的情况下,来确定引用的生命周期 -
规则
1
:应用于输入生命周期 -
规则
2、3
:应用于输出生命周期 -
如果编译器应用完
3
个规则后,仍然有无法确定生命周期的引用------>报错 -
这些规则适用于
fn
定义和impl
块 -
规则
1
:每个引用类型的参数都有自己的生命周期 -
规则
2
:如果只有 1 个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数 -
规则
3
:如果有多个输入生命周期参数,但其中一个是&self
或&mut self
(是一个对象的方法:在结构体、枚举类型、trait对象中的函数被称为方法)
那么 self
的生命周期会被赋给所有输出生命周期参数
例子1:
- 假设我们自己就是编译器
- 我们应用这些规则来计算上面
first_word
函数签名中的引用的生命周期
开始时签名中的引用并没有关联任何生命周期:
rust
fn first_word(s: &str) -> &str {
接着编译器应用第一条规则,也就是每个引用参数都有其自己的生命周期。我们像往常一样称之为 'a
,所以现在签名看起来像这样:
rust
fn first_word<'a>(s: &'a str) -> &str {
对于第二条规则,因为这里正好只有一个输入生命周期参数所以是适用的。第二条规则表明输入参数的生命周期将被赋予输出生命周期参数,所以现在签名看起来像这样:
rust
fn first_word<'a>(s: &'a str) -> &'a str {
现在这个函数签名中的所有引用都有了生命周期,如此编译器可以继续它的分析而无须程序员显式的标记这个函数签名中的生命周期。
例子2
再次假设自己是编译器
rust
fn longest(x: &str, y: &str) -> &str {
应用第一条规则:每个引用参数都有其自己的生命周期。这次有两个参数,所以就有两个(不同的)生命周期:
rust
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
应用第二条规则,因为函数存在多个输入生命周期,它并不适用于这种情况,所以用第三条规则,当然第三条规则也不适用,因为这是个函数没有self
,所以返回的&str
没有生命周期,没有把所有的引用都标记生命周期,会报错,让你手动添加生命周期
**注意:**函数和方法,在java中表示的是一个,但是在Rust中,在结构体(或者枚举类型、trait对象)中的函数被称为方法,剩下的叫函数
方法定义中的生命周期标注和省略
- 在
struct
上使用生命周期实现方法,语法和第十三章 -->2)泛型--> 在方法中使用泛型
是一样的 - 想让
struct
内部有引用参数,就必须声明生命周期,在代码第1-3
行 impl
之后和类型名称之后的生命周期参数是必要的代码,第5
行:impl<'a> ImportantExcerpt<'a>
impl
内部的self
必须要标注生命周期,但是因为生命周期规则我们可以省略,第6
行:fn level(&self) -> i32 {
rust
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
1
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part1(&self) -> &str {
self.part
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part2(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
fn main() {
let x = ImportantExcerpt { part: "2" };
println!("{}", x.level());
println!("{}", x.announce_and_return_part1());
println!("{}", x.announce_and_return_part2("3"));
}
输出:
rust
1
2
Attention please: 3
2
代码说明:
第1-3
行:声明一个内部有引用类型的struct
,如果有引用类型就必须标注生命周期
第5-9
行:实现了ImportantExcerpt
,创建了一个方法level
,因为返回值是i32
,不是引用不涉及生命周期,因为生命周期存在的意义就是,如果你用到一个返回值,而这个返回值出现悬垂引用的现象,现在返回值一直存在,所以代码第6
行,fn level(&self)
不必写为fn level(&'a self)
第11-15
行:
按编译器应用第一条规则,每个输入生命周期的引用参数都有其自己的生命周期
rust
fn announce_and_return_part1(&'a self) -> &str
按编译器应用第二条规则,只有 1 个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数
rust
fn announce_and_return_part1(&'a self) -> &'a str
所有的引用生命周期都能确定,那么就可以省略,所以可以写为这样
rust
fn announce_and_return_part1(&self) -> &str {
第17-22
行:
按编译器应用第一条规则,每个输入生命周期的引用参数都有其自己的生命周期
rust
fn announce_and_return_part2(&'a self, announcement: &'a str) -> &str {
按编译器应用第三条规则,如果有多个输入生命周期参数,但其中一个是&self
或&mut self
,那么 self
的生命周期会被赋给所有输出生命周期参数
rust
fn announce_and_return_part2(&'a self, announcement: &'a str) -> &'a str {
所有的引用生命周期都能确定,那么就可以省略,所以可以写为这样
rust
fn announce_and_return_part2(&self, announcement: &str) -> &str {
9)静态生命周期
'static
是一个特殊的生命周期,表示:整个程序的执行期- 所有的字符串字面值都拥有
'static
生命周期,我们也可以选择像下面这样标注出来: - 这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是
'static
的。
rust
fn main() {
let s: &'static str = "I have a static lifetime.";
}
你可能在错误信息的帮助文本中见过使用 'static
生命周期的建议,不过将引用指定为 'static
之前,思考一下这个引用是否真的在整个程序的生命周期里都有效。你可能会考虑希望它一直有效,如果可能的话。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个 'static
的生命周期。
10)结合泛型类型参数、trait bounds 和生命周期
rust
use std::fmt::Display;
fn ptn<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
println!("{}", ptn("abc", "ab", "ac"))
}
输出:
rust
Announcement! ac
abc
代码说明:
第3-5
行:一个叫ptn
的方法,传入的参数,x
,y
,和一个实现了Display
这个trait
参数T
,返回了一个&str
为什么不忽略'a
按编译器应用第一条规则,每个输入生命周期的引用参数都有其自己的生命周期
rust
fn ptn<T>(x: &'a str, y: &'a str, ann: T) -> &str
编译器无法应用第二条规则,因为有多个参数
编译器无法应用第三条规则,有多个参数但是没有self
所以在编译器眼中,返回参数是&str
,没有生命周期,所以你必须要显式的标注生命周期
团队就把这些模式编码进了 Rust 编译器中,如此借用检查器在这些情况下就能推断出生命周期而不再强制程序员显式的增加注解。
这里我们提到一些 Rust 的历史是因为更多的明确的模式被合并和添加到编译器中是完全可能的,未来需要手动标注的生命周期会越来越少。
生命周期省略规则
- 在Rust引用分析中所编入的模式称为:生命周期省略规则
- 这些规则无需开发者来遵守
- 他们是一些特殊情况,由编译器来考虑
- 如果你的代码符合这些情况,那么就无需显式标注生命周期
- 生命周期省略规则不会提供完整的推断:
- 如果应用规则后,引用的生命周期仍然模糊不清------>编译错误
- 解决办法:添加生命周期标注,表明引用间的相互关系
输入、输出生命周期
- 生命周期在:
- 函数/方法的参数:输入生命周期
- 函数/方法的返回值:输出生命周期
生命周期省略的三个规则
-
编译器使用
3
个规则在没有显式标注生命周期的情况下,来确定引用的生命周期 -
规则
1
:应用于输入生命周期 -
规则
2、3
:应用于输出生命周期 -
如果编译器应用完
3
个规则后,仍然有无法确定生命周期的引用------>报错 -
这些规则适用于
fn
定义和impl
块 -
规则
1
:每个引用类型的参数都有自己的生命周期 -
规则
2
:如果只有 1 个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数 -
规则
3
:如果有多个输入生命周期参数,但其中一个是&self
或&mut self
(是一个对象的方法:在结构体、枚举类型、trait对象中的函数被称为方法)
那么 self
的生命周期会被赋给所有输出生命周期参数
例子1:
- 假设我们自己就是编译器
- 我们应用这些规则来计算上面
first_word
函数签名中的引用的生命周期
开始时签名中的引用并没有关联任何生命周期:
rust
fn first_word(s: &str) -> &str {
接着编译器应用第一条规则,也就是每个引用参数都有其自己的生命周期。我们像往常一样称之为 'a
,所以现在签名看起来像这样:
rust
fn first_word<'a>(s: &'a str) -> &str {
对于第二条规则,因为这里正好只有一个输入生命周期参数所以是适用的。第二条规则表明输入参数的生命周期将被赋予输出生命周期参数,所以现在签名看起来像这样:
rust
fn first_word<'a>(s: &'a str) -> &'a str {
现在这个函数签名中的所有引用都有了生命周期,如此编译器可以继续它的分析而无须程序员显式的标记这个函数签名中的生命周期。
例子2
再次假设自己是编译器
rust
fn longest(x: &str, y: &str) -> &str {
应用第一条规则:每个引用参数都有其自己的生命周期。这次有两个参数,所以就有两个(不同的)生命周期:
rust
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str {
应用第二条规则,因为函数存在多个输入生命周期,它并不适用于这种情况,所以用第三条规则,当然第三条规则也不适用,因为这是个函数没有self
,所以返回的&str
没有生命周期,没有把所有的引用都标记生命周期,会报错,让你手动添加生命周期
**注意:**函数和方法,在java中表示的是一个,但是在Rust中,在结构体(或者枚举类型、trait对象)中的函数被称为方法,剩下的叫函数
方法定义中的生命周期标注和省略
- 在
struct
上使用生命周期实现方法,语法和第十三章 -->2)泛型--> 在方法中使用泛型
是一样的 - 想让
struct
内部有引用参数,就必须声明生命周期,在代码第1-3
行 impl
之后和类型名称之后的生命周期参数是必要的代码,第5
行:impl<'a> ImportantExcerpt<'a>
impl
内部的self
必须要标注生命周期,但是因为生命周期规则我们可以省略,第6
行:fn level(&self) -> i32 {
rust
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
fn level(&self) -> i32 {
1
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part1(&self) -> &str {
self.part
}
}
impl<'a> ImportantExcerpt<'a> {
fn announce_and_return_part2(&self, announcement: &str) -> &str {
println!("Attention please: {}", announcement);
self.part
}
}
fn main() {
let x = ImportantExcerpt { part: "2" };
println!("{}", x.level());
println!("{}", x.announce_and_return_part1());
println!("{}", x.announce_and_return_part2("3"));
}
输出:
rust
1
2
Attention please: 3
2
代码说明:
第1-3
行:声明一个内部有引用类型的struct
,如果有引用类型就必须标注生命周期
第5-9
行:实现了ImportantExcerpt
,创建了一个方法level
,因为返回值是i32
,不是引用不涉及生命周期,因为生命周期存在的意义就是,如果你用到一个返回值,而这个返回值出现悬垂引用的现象,现在返回值一直存在,所以代码第6
行,fn level(&self)
不必写为fn level(&'a self)
第11-15
行:
按编译器应用第一条规则,每个输入生命周期的引用参数都有其自己的生命周期
rust
fn announce_and_return_part1(&'a self) -> &str
按编译器应用第二条规则,只有 1 个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数
rust
fn announce_and_return_part1(&'a self) -> &'a str
所有的引用生命周期都能确定,那么就可以省略,所以可以写为这样
rust
fn announce_and_return_part1(&self) -> &str {
第17-22
行:
按编译器应用第一条规则,每个输入生命周期的引用参数都有其自己的生命周期
rust
fn announce_and_return_part2(&'a self, announcement: &'a str) -> &str {
按编译器应用第三条规则,如果有多个输入生命周期参数,但其中一个是&self
或&mut self
,那么 self
的生命周期会被赋给所有输出生命周期参数
rust
fn announce_and_return_part2(&'a self, announcement: &'a str) -> &'a str {
所有的引用生命周期都能确定,那么就可以省略,所以可以写为这样
rust
fn announce_and_return_part2(&self, announcement: &str) -> &str {
9)静态生命周期
'static
是一个特殊的生命周期,表示:整个程序的执行期- 所有的字符串字面值都拥有
'static
生命周期,我们也可以选择像下面这样标注出来: - 这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是
'static
的。
rust
fn main() {
let s: &'static str = "I have a static lifetime.";
}
你可能在错误信息的帮助文本中见过使用 'static
生命周期的建议,不过将引用指定为 'static
之前,思考一下这个引用是否真的在整个程序的生命周期里都有效。你可能会考虑希望它一直有效,如果可能的话。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个 'static
的生命周期。
10)结合泛型类型参数、trait bounds 和生命周期
rust
use std::fmt::Display;
fn ptn<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
where
T: Display,
{
println!("Announcement! {}", ann);
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
println!("{}", ptn("abc", "ab", "ac"))
}
输出:
rust
Announcement! ac
abc
代码说明:
第3-5
行:一个叫ptn
的方法,传入的参数,x
,y
,和一个实现了Display
这个trait
参数T
,返回了一个&str
为什么不忽略'a
按编译器应用第一条规则,每个输入生命周期的引用参数都有其自己的生命周期
rust
fn ptn<T>(x: &'a str, y: &'a str, ann: T) -> &str
编译器无法应用第二条规则,因为有多个参数
编译器无法应用第三条规则,有多个参数但是没有self
所以在编译器眼中,返回参数是&str
,没有生命周期,所以你必须要显式的标注生命周期
然后这个方法还有个参数还有个T
,函数声明就要写成<'a, T>
这样