【2025rust笔记】超详细,小白,rust基本语法
一、常见cargo命令
- 查看cargo版本 cargo --version
- 创建cargo项目 create new demo_name
- 构建编译项目 cargo build
- 运行项目 cargo run
- 检查项目代码 cargo check (比cargobuild快)
- 发布构建项目 cargo build --release
电子markdown/pdf格式
二、小demo-----猜数游戏
1、print
mut 将 “不可变值” 变为 “可变值”
将a的值赋值给blet a = 1;let b = a;println!("{}",a);//输出宏的传参println!("{}",b);
输出:11如果用另一种方式,先声明,再赋值let a = 1;let b = 0;b = a;
会报错,因为所有的声明,默认是 immutable 不可变的
需要加 mutlet a = 1;let mut b = 0;b = a;println!("{}",a);println!("{}",b);注意:不能 let b ,所有的声明都要有初始值
读取输入
io::stdin().read_line(&mut guess).expect("无法读取行");
如果read_line()
返回的io::Result
实例的值是Ok,那么expect()
会提取Ok的附加值,作为结果返回给用户
如果read_line()
返回的io::Result
实例的值是Err,那么expect()
会把程序中断,并把传入的字符串信息显示出来
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坐标导入)
[dependencies]
rand = "^0.3.14"
^表示任何与这个版本公共API兼容的版本都可以
随后重新启动包:
Ctrl + Shift + P
>rust
Rust: Start the Rust server
代码
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、比较生成的随机数和输入数字
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、多次猜测
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、变量与可变性
变量:
let a = 1; // 用let修饰,此时是不可以修改的
let mut ab = 1; // 加上mut之后,变量可以进行修改
常量:(大写字母+数字下划线)
const MAX_POINTS: u32 = 10_0000;fn main() { const MAX_POINTS: u32 = 10_0000; }
Shadowing
可以使用相同的名字声明新的变量,新的变量就会 shadow(隐藏) 之前声明的同名变量
在后续的代码中这个变量名就是新的变量
shadow :
fn main() {let a = 2;let a = a + 1;let a = a * 3;println!("The value a is :{}",a);}
mut
:
fn main() {let mut a = 2;a = a + 1;a = a * 3;println!("The value a is :{}",a);}
2、数据类型
Rust 是静态编译语言,在编译时必须知道所有变量的类型
-
基于使用的值,编译器通常能够推断出它的具体类型 Ctrl+左键 查看示例
-
但如果可能的值比较多(例如把
String
转成整数的parse()
方法),就必须添加类型的标注,否则编译会报错
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位,双精度
let a = 1.0; //f64
let a :f32 = 1.0; //f32
Rust 的浮点类型使用了 IEEE-754 标准来表述
默认会使用 f64
类型
数值操作
加减乘除余
跟其他语言一样,不多赘述
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
let t = true;let f :bool = false;
字符类型
请注意字符类型是单引号
如果let b :char = "₦"
这样声明会报错
let a = 'n';let b :char = '₦';let c = ' ';
2)复合类型
- 复合类型可以将多个值放在一个类型里
- Rust 提供了两种基础的复合类型:元组(Tuple)、数组
元组(Tuple)
- Tuple 可以将多个类型的多个值放在一个类型里
- Tuple 的长度是固定的:一旦声明就无法改变
创建Tuple
- 在小括号里,将值用逗号隔开
- Tuple 中的每个位置都对应一个类型,Tuple中各元素的类型不必相同
let tup: (i32,f64,char) = (100,5.1,'a');//创建Tupleprintln!("{},{},{}",tup.0,tup.1,tup.2);
获取Tuple的元素值
- 可以使用模式匹配来解构(destructure)一个Tuple 来获取元素的值
let tup: (i32,f64,char) = (100,5.1,'a');let (x, y, z) = tup;//给变量赋值println!("{},{},{}", x, y, z);
访问Tuple的元素
- 在 tuple 变量使用点标记法,后接元素的索引号
let tup: (i32,f64,char) = (100,5.1,'a');println!("{},{},{}",tup.0,tup.1,tup.2);//访问 Tuple 的元素
数组
let a = [1, 2, 3, 4];
3、函数和注释
1)函数
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
的值是()
fn main() {let x = 5;let y = {let x = 1;x + 3};println!("{}", y);//输出 4
}
函数的返回值
fn main() {println!("{}", f(3));}fn f(x: i32) -> i32 {x + 5}
返回多个值
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)注释
跟正常的语言一样
/*
*注释
*///注释
4、控制流
1)if
else
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
来重构代码
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
语句中等号的右边
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
关键字来告诉程序何时停止循环
fn main() {let mut a = 0;let result = loop {a += 1;if a == 10 {break a * 2;}};println!("{}", result);
}
while
- 每次执行循环体之前都判断一次条件
fn main() {let mut number = 3;while number != 0 {println!("{}", number);number -= 1;}println!("结束")
}
for
- while 和 loop 也能实现遍历循环,但是for 不需要写判断,也就不会出现数组越界的情况,还有就是速度比较快,所以 Rust 用来遍历的还是用
for
fn main() {let a = [10, 20, 30, 40, 50];for element in a.iter() {println!("value is {}", element);}
}
- 把数组中的值,同步加50
fn main() {let mut v = [100, 32, 57];for i in &mut v{*i += 50;}for i in v{println!("{}",i);}
}
输出:
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 就是程序中一个有效范围
- 跟别的语言一样
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 类型下的函数- 这类字符串是可以被修改的
fn main() {let mut s = String::from("hello");s.push_str(" word");println!("{}", s);}
5、所有权与函数(例子)
- 在语义上,将值传递给函数和把值赋给变量是类似的
- 将值传递给函数将发生移动和复制
例子:
fn main() {let s = String::from("Hello World");//这里声明引用类型,String,take_ownership(s);//放入函数,发生了移动let a = 1;//声明整型makes_copy(a);//实际上传入的是a的副本}//a:在Stack中的本来数据被dropfn take_ownership(some_string: String) {println!("{}", some_string);}//s:这里Heap中的数据被drop了fn makes_copy(some_number: u32) {println!("{}", some_number);}//a:在Stack中的副本数据被drop
6、返回值与作用域
函数在返回值的过程中同样也会发生所有权的转移
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、让函数使用某个值,但不获得所有权
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)引用
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)借用
当一个函数使用引用,而不是一个真实的值作为它的参数,我们就管这个行为叫做借用
修改借用的数据
那我们是否可以修改借用的东西呢?
-
不可以
-
和变量一样,引用默认也是不可变的
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()
}
那么我们把引用的变为可变的,是否就可以修改了呢
这样就不会报错了
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()
}
修改借用的数据时的限制
但是有个重要的限制:在特定作用域内,对某一块数据,只能有一个可变的引用
- 这样做的好处就是可以在编译时防止数据竞争
- 以下三种行为会发生数据竞争:
- 两个或多个指针同时访问一个数据
- 至少有一个指针用于写入数据
- 没有使用任何的机制来同步对数据的访问
fn main() {let mut s = String::from("Hello");let s1 = &mut s;let s2 = &mut s;//这里会报错告诉你只能用一个println!("{}, {}", s1, s2);
}
我们可以通过创建新的作用域,来允许非同时的创建多个可变引用
就像这样
fn main() {let mut s = String::from("Hello");{let s1 = &mut s;//s1 就会在这个作用域存在}let s2 = &mut s;
}
还有另一个限制
- 不可以同时拥有一个可变引用和一个不变的引用
- 多个不变的引用是可以的
例子:
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 会通过一个编译时错误来避免:
fn main() {let r = dangle();}fn dangle() -> &String {let s = String::from("hello");&s}//离开这个方法作用域,s销毁了,而这个方法,返回了s的引用,也就是说,会指向一个已经被释放的内存空间,所以会直接报错
七、切片
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 )
左闭右开,内部数字为引用字符串索引
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
struct User {username: String,email: String,sign_in_count: u64,active: bool,}
实例化struct
let user1 = User{email:String::from("1841632321@qq.com"),username:String::from("李泽辉"),sign_in_count:1,active:true,
}
取得 struct 里面的某个值
- 使用点标记法
println!("{}", user1.username);println!("{}", user1.email);println!("{}", user1.sign_in_count);println!("{}", user1.active);
修改 struct 里面的某个值
- 修改了
username
,注意要给实例user1
加mut
因为是可变的 - 而一旦这个实例
user1
是可变的,那么示例中的所有字段都是可变的
let mut user1 = User {email: String::from("1841632321@qq.com"),username: String::from("李泽辉"),sign_in_count: 1,active: true,};
struct 作为函数的返回值
- 传入用户名和邮箱,返回用户的结构体
fn return_user(email: String, username: String) -> User {User {email: email,username: username,sign_in_count: 1,active: true,}}
字段初始化简写
- 当字段名与字段值对应的变量名相同时,就可以使用字段初始化简写的方式
- 比如上面的例子,传入用户名和邮箱,结构体字段名和传入字段名一样,可以直接简写为下面的样子
fn return_user(email: String, username: String) -> User {User {email,username,sign_in_count: 1,active: true,}}
struct 更新语法
- 当你想基于某个 struct 实例来创建一个新实例的时候,可以使用 struct 更新语法:
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
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 的例子
例子需求
计算长方形面积
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 上面的方法
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());
}
带有更多参数的方法
我们要实现一个功能,判断一个长方形,是否能容纳下另一个长方形
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代表rec2println!("rec1能否包括rec3:{}", rec1.can_hold(&rec3));
}
返回:
rec1能否包括rec2:true
rec1能否包括rec3:false
关联函数
#[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)枚举
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) {}
将数据附加到枚举的变体中
所有类型都可以进行附加数据
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(预导入模块)中
- 描述了:某个值 可能存在(某种类型)或不存在的情况
enum Option<T> {Some(T),None,
}
可以直接使用,不需要像正常的枚举一样,Option::Some(5);
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:
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");}}
}
运行结果:
Hello
//opt is nothing
3)控制流运算符 - match
- 允许一个值与一系列模式进行匹配,并执行匹配的模式对应的代码
- 模式可以是字面值、变量名、通配符
有一个结构体Coin
里面四个变体,对应四个分支返回值
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);
}
输出:
1
绑定值的模式匹配(提取enum中的变体)
- 匹配的分支可以绑定到被匹配对象的部分值
- 因此可以从 enum 变体中提取值
#[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)); //取值
}
输出:
state is Alaska {x: 10,y: 20,
}
25
10
匹配Option<T>
fn main() {let five = Some(5); //定义一个Optionlet six = plus_one(five); //走Some分支,i+1let 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
必须都有分支
fn main() {}fn plus_one(x: Option<i32>) -> Option<i32> {match x {None => None,Some(i) => Some(i + 1),}
}
match使用通配符_
不用穷举所有可能性了
fn main() {let v = 4;match v {1 => println!("1"),3 => println!("2"),_ => println!("other"),}
}
_
表示除了以上两种情况外,剩下所有的
if let
处理只关心一种匹配,忽略其他匹配的情况,你可以认为他是只用来区分两种情况的match
语句的语法糖
语法格式:
if let 匹配值 = 源变量 {语句块
}
用match
来写,如果i
是0
,输出0
,其他数字输出other
fn main() {let i = 0;match i {0 => println!("zero"),_ => println!("other"),}
}
我们用if let
试一下
fn main() {let i = 0;if let 0 = i {println!("zero")} else {println!("other")}
}
输出:
zero
上面的是标量,我们现在用枚举试一下
fn main() {enum Book {Papery(u32),Electronic,}let book = Book::Papery(1);if let Book::Papery(index) = book {println!("{}", index)} else {println!("Electronic")}
}
输出:
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
模块。模块还可以保存一些定义的其他项,比如结构体、枚举、常量、特性、或者函数。
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的模块树
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 或当前模块的标识符
-
路径至少由一个标识符组成,标识符之间使用 ::
-
如果定义的部分和使用的部分总是一起移动,用相对路径,可以独立拆解出来,用绝对路径
例子:
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的用法
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
mod back_of_house {pub struct Breakfast {pub x: String,//公有y: String,//私有}
}
pub enum
mod back_of_house {pub enum Appetizer {Soup,Salad,}
}
5)use
关键字
- 可以使用
use
关键字将路径导入到作用域内 - 仍然遵守私有性规则
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();
}
-
函数:将函数的父级模块引入作用域是常用做法
-
下面这种做法可以,但并不是习惯方式。
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
,其他:指定完整路径(指定到本身)
use std::collections::HashMap;fn main() {let mut map = HashMap::new();//直接指定到方法map.insert(1, 2);
}
- 有一种情况两个不同的类,下面有同名的方法,我们不能指定本身,要加上父级路径
use std::fmt;
use std::io;fn f1() -> fmt::Result {}//会报错因为没有返回值fn f2() -> io::Result {}//会报错fn main() {}
6)as
我们有另外一种做法as
- as关键字可以为引入的路径指定本地的别名
use std::fmt::Result;
use std::io::Result as IoResult;fn f1() -> Result {}fn f2() -> IoResult {}fn main() {}
使用 pub use
重新导出名称
- 使用
use
将路径(名称)导入到作用域内后,该名称在此作用域内是私有的 - 可以将条目引入作用域
- 该条目可以被外部代码引入到它们的作用域
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
,外部代码也可以使用这个路径。
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文件添加依赖的包
[dependencies]
rand = "0.5.5"
- use 将特定条目引入作用域
- 标准库(std)也被当作外部包,但是不需要修改dependencies来包含它
9)使用嵌套路径清理大量的 use 语句
-
如果使用同一个包或模块下的多个条目
-
可以使用嵌套路径,在同一行内将上述条目进行引入
-
路径相同的部分 : : { 路径差异的部分 }
use std::cmp::Ordering;
use std::io;
变为
use std::{cmp::Ordering, io};
特殊情况:
use std::io;
use std::io::Write;
变为
use std::io::{self, Write};
10)通配符
我么可以使用 * 把路径中所有的公共条目都引入到作用域
把这个路径下的所有都引入了
use std::collections::*;
谨慎使用
- 应用场景:
- 测试:将所有被测试代码引入tests模块
- 有时被用于预导入(prelude)模块
11)将模块内容移动到其他文件
- 模块定义时,如果模块名后边是 " ; " ,而不是代码块
- Rust 会从与模块同名的文件中加载内容
- 模块树的结构不会变化
两层分离
初始内容( lib.rs文件 )
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
文件中
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
文件中
pub mod hosting {pub fn add_to_waitlist() {}
}
三层分离
如果想把,hosting 里面的内容再次独立出来
新建一个 front_of_house 的文件 ,里面写上hosting.rs
hosting.rs
内容
pub fn add_to_waitlist() {}
front_of_house
内容
pub mod hosting;
lib.rs
内容
mod front_of_house;use crate::front_of_house::hosting;pub fn eat_at_restaurant() {hosting::add_to_waitlist();
}
十一、常用的集合
1)Vector
fn main() {let v: Vec<i32> = Vec::new();
}
- 使用初始值创建
Vet<T>
,使用 vec! 宏
fn main() {let v = vec![1,2,3];
}
更新Vector
- 向 Vector 添加元素,使用 push 方法
fn main() {let mut v = Vec::new();
}
先写一行,你会发现这次我没有写里面的类型,他现在是会报错的,因为rust无法推断vector的类型
我们用push
fn main() {let mut v = Vec::new();v.push(1);v.push(2);
}
你发现不报错了,因为vector里有数据了,rust可以推断类型了
删除Vector
- 与任何其他的 struct 一样,当 Vector 离开作用域后
- 它就被清理掉了
- 它所有的元素也被清理掉了
fn main() {let v = vec![1,2,3];
}//到这里就自动被清理了
但是如果涉及到对 vector 里面的元素有引用的话,就会变复杂
读取 Vector 的元素
- 两种方式可以引用 Vector 里的值
- 索引
- get 方法
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."),
}
}
输出:
The third element is 3
The third element is 3
如果我们超出了索引
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中也是适用的,不能在同一作用域内同时拥有可变和不可变引用
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 循环
fn main() {let mut v = vec![100, 32, 57];for i in &mut v{*i += 50;}for i in v{println!("{}",i);}
}
输出
150
82
107
2)Vector + Enum 的例子
vector 只能储存相同类型的值。这是很不方便的;绝对会有需要储存一系列不同类型的值的用例。幸运的是,枚举的成员都被定义为相同的枚举类型,所以当需要在 vector 中储存不同类型值时,我们可以定义并使用一个枚举。
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()
函数
fn main() {let mut s = String::new();//std::string::String
}
- 使用初始值来创建String
1、这新建了一个叫做 s
的空的字符串,接着我们可以向其中装载数据。可以使用 to_string
方法,它能用于任何实现了 Display
trait 的类型,字符串字面值也实现了它。
fn main() {let data = "initial contents";//&str 类型let s = data.to_string();//std::string::Stringlet s1 = "initial contents".to_string();//std::string::String
}
2、直接使用String::from()
fn main() {let s = String::from("AAA");//std::string::String
}
更新String
push_str()
方法:
把一个字符串切片附加到 String
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
fn main() {let mut a = String::from("AAA");a.push('B');
}
+
拼接字符串
fn main() {let s1 = String::from("Hello, ");let s2 = String::from("World!");let s3 = s1 + &s2;println!("{}", s3);//Hello, Worldprintln!("{}", 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!:
连接多个字符串
如果用 +
连接多个显得很笨重
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!
这个宏
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 的某部分,会报错
let s1 = String::from("hello");
let h = s1[0];
Rust 的字符串不支持索引。那么接下来的问题是,为什么不支持呢?
fn main() {let len1 = String::from("Hola").len();let len2 = String::from("Здравствуйте").len();println!("{}", len1);println!("{}", len2);}
输出:
4
24
String
是一个 Vec<u8>
的封装。
“Hola” 的 Vec
的长度是四个字节:这里每一个字母的 UTF-8 编码都占用一个字节。
"Здравствуйте"是中如果你要返回З
你需要返回两个字节,那么你返回哪一个呢?
为了避免返回意外的值并造成不能立刻发现的 bug,Rust 根本不会编译这些代码,并在开发过程中及早杜绝了误会的发生。
还有一个原因是,索引操作预期总是需要常数时间 (O(1))。但是对于 String
不可能保证这样的性能,因为 Rust 必须从开头到索引位置遍历来确定有多少有效的字符。
字节、标量和字形簇
- Rust 有三种看待字符串的方式:
- 字节
- 标量值
- 字形簇(最接近所谓的“字母”)
循环输出字节:
fn main() {let w = "नमस्ते";for b in w.bytes(){println!("{}",b)}}
输出:
224
164
168
224
164
174
224
164
184
224
165
141
224
164
164
224
165
135
循环输出标量值
fn main() {let w = "नमस्ते";for b in w.chars(){println!("{}",b)}}
输出:
न
म
स
्
त
े
那两个特殊的需要结合字符才表示意义,
字形簇才是所谓的四个字符“न म स त”
有需要可以去https://crates.io/这里找
切割String
- 可以使用【】和 一个范围 来创建字符串的切片
请看前面的第七章:切片
4)HashMap
-
键值对的形式存储数据,一个 Key 对应一个 Value
-
Hash 函数:决定如何在内存中存放 K 和 V
-
适用场景:通过 K(任何类型)来寻找数据,而不是通过索引
-
HashMap 是同构的,所有的 K 是同一类型,所有的 V 是同一类型
创建 HashMap
- 创建空的 HashMap:new()函数
- 添加数据:insert()方法
- 输出值:get()和 unwrap()方法
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的循环输出
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);}
}
运行结果:
("color", "red")
("size", "10 m^2")
另一种创建HashMap的方式
- 在元素类型为 Tuple 的 Vector 上使用 collect 方法,可以组建一个 HashMap:
- 要求 Tuple 要有两个值:一个作为K,一个作为V
- collect 方法可以把数据整合成很多集合类型,包括 HashMap
- 返回值需要显式的指明类型
.iter()
返回遍历器,使用zip()
语法,就可以创建一个元组的数组,再用collect()
就能创建一个HashMap
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);}
}
输出:
("Blue", 10)
("Yellow", 50)
这里 HashMap<_, _>
类型注解是必要的,因为可能 collect
很多不同的数据结构,而除非显式指定否则 Rust 无从得知你需要的类型。但是对于键和值的类型参数来说,可以使用下划线占位,而 Rust 能够根据 vector 中数据的类型推断出 HashMap
所包含的类型。
HashMap的所有权
- 对于实现了Copy trait 的类型(例如 i32),值会被复制到 HashMap中
- 对于拥有所有权的值(例如 String),值会被移动,所有权会转移给HashMap
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有效期内,被引用的值必须保持有效
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>
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"),}
}
输出:
10
遍历HashMap
- for 循环
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);}
}
输出:
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 会被替换掉
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);
}
输出:
{"Blue": 50}
在 K 不对应任何 V 的情况下,才插入V
entry
方法:检查指定的 K 是否对应一个 V- 参数为 K
- 返回 enum Entry:代表这个值是否存在
or_insert()
- 返回:
- 如果 K 存在,返回到对应的 V 的一个可变引用
- 如果 K 不存在,将参数方法作为 K 的新值插进去,返回到这个值的可变引用
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);
}
输出:
{"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
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);
}
输出:
{"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 时直接终止:
一个小例子
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 中元素的例子:
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(预导入模块)带入作用域
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!
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
表达式
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 表达式的一个快捷方法
- 打开文件,如果成功打开,返回文件,打开失败,返回异常
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! 宏,中止程序 。
use std::fs::File;
use std::io::ErrorKind;fn main() {let test2 = File::open("hello.txt").unwrap();}
输出这样的报错信息,这个报错信息我们无法自定义,这也是unwrap
的缺点
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
类似,但是它可以在其基础上指定错误信息
use std::fs::File;
use std::io::ErrorKind;fn main() {let test2 = File::open("hello.txt").expect("打开文件出错啦!!");}
输出:
thread 'main' panicked at '打开文件出错啦!!: Os { code: 2, kind: NotFound, message: "系统找不到指定的
文件。" }', src\main.rs:5:41
传播错误(包含Result作为函数返回值写法)
之前所讲的是接收到错误的处理方式,但是如果我们自己编写一个函数在遇到错误时想传递出去怎么办呢?
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
也可以
match f.read_to_string(&mut s) {Ok(_) => Ok(s),Err(e) => Err(e),}
因为最后一个match
表达式,不用写return
就可以表示为函数的返回值
?
运算符
?
运算符:传播错误的一种快捷方式- 如果
Result
是Ok
:Ok
中的值就是表达式的结果,然后继续执行程序 - 如果
Result
是Err
:Err
就作为整个函数的返回值返回
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
函数 -
用于:针对不同错误原因,返回同一种错误类型
?
运算符的链式调用
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
的函数
use std::fs::File;fn main() {let f = File::open("hello.txt")?;
}
运行报错为:?
运算符只能用于函数返回结果是Result
、Option
或者是实现了FromResidual
的类型
?
运算符与main函数
- main 函数返回类型是
()
- main 函数的返回类型也可以是:
Result<T,E>
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)提交函数消除重复代码(包含引用与解引用)
初始代码
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
行:重复代码
简化代码
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
类型的进行比较和赋值了
如果把代码变为这样
fn largest(list: &[i32]) -> i32 {let mut largest = list[0];for item in list {if item > largest {//报错largest = item;//报错}}largest
}
因为item
是&i32
类型,largest
是i32
类型,无法进行比较和赋值
我们可以这样写
fn largest(list: &[i32]) -> i32 {let mut largest = list[0];for item in list {if item > &largest {largest = *item}}largest
}
或者是这样,用*
进行解引用
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
中的最大值输出出来
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);
}
输出:
100
1000
现在我们在第16
行把Vector
变为字符的集合
第17
行会报错
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
行会报错
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
(结构体)中定义的泛型
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 他期望是一个整数
struct Point<T> {x: T,y: T,
}
fn main() {let integer = Point { x: 5, y: 10.0 };
}
我们这样就能够解决了
struct Point<T, U> {x: T,y: U,
}
fn main() {let integer = Point { x: 5, y: 10.0 };
}
在Enum(枚举)中使用泛型
类似于结构体,枚举也可以在其成员中存放泛型数据类型。第六章我们使用过了标准库提供的 Option<T>
枚举,让我们再看看:
enum Option<T> {Some(T),None,
}
现在这个定义看起来就更容易理解了。如你所见 Option<T>
是一个拥有泛型 T
的枚举,它有两个成员:Some
,它存放了一个类型 T
的值,和不存在任何值的None
。通过 Option<T>
枚举可以表达有一个可能的值的抽象概念,同时因为 Option<T>
是泛型的,无论这个可能的值是什么类型都可以使用这个抽象。
枚举也可以拥有多个泛型类型
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
实现方法的时候,可以使用泛型
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> {...}
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
里的泛型类型参数如果和方法的泛型类型参数不同
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);
}
输出:
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),就是在编译时就将泛型替换为具体类型的过程
下面的代码在编译时会怎么样呢?
let integer = Some(5);
let float = Some(5.0);
当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给 Option<T>
的值并发现有两种 Option<T>
:一个对应 i32
另一个对应 f64
。为此,它会将泛型定义 Option<T>
展开为 Option_i32
和 Option_f64
,接着将泛型定义替换为这两个具体的定义。
编译器生成的单态化版本的代码看起来像这样
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
文件
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
文件
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
输出:
我们来用一下tweet: 用户名, 显示content
总结:其实就是lib.rs
中Tweet
这个struct
实现了Summary
这个trait
,然后在main.js
中声明一个tweet
的实例,可以用Summary
里面的方法
实现trait的约束
-
当某一个类型实现了某个
trait
的前提条件是:这个类型或者是这个trait
在本地的crate
里定义了 -
要么是类型(例如struct)在本地定义,我们去实现外部的trait,要么是trait是在本地定义的,我们是用外部的类型(struct)去实现本地的trait
-
如果两个都是外部的,外部的类型去实现外部的trait是不可以的
默认实现
这就是与接口不同的地方,trait可以自己定义一个方法作为默认方法
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());
}
输出:
lizehui 24
如果我们去掉代码第13-15
行,他就会实现默认方法
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());
}
输出:
输出:[Object]
如果我们在trait中嵌套使用方法呢,这样会报错
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
实现才能消除错误,像这样
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());
}
输出:
输出:111
4)Trait(下)
Trait作为参数
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));
}
输出:
输出: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
行:输出
语法糖
fn output(object: impl Descriptive) -> String {object.describe()
}
可以等效的写为,这被称为trait bound
fn output<T: Descriptive>(object: T) -> String {object.describe()
}
传多个参数
如果传多个参数,在代码的第25
行,会很长
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
行精简了很多
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
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));
}
输出:
输出: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
行改变
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));
}
输出:
输出: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
,很长难以阅读
fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {//方法中的内容
}
简化写法:
fn some_function<T, U>(t: T, u: U) -> i32
where T: Display + Clone,U: Clone + Debug
{//方法中的内容
}
用Trait
作为返回类型
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());
}
输出:
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
,但是也是不行的
fn some_function(bool bl) -> impl Descriptive {if bl {return A {};} else {return B {};}
}
解决之前传参T无法用>
比较的问题
关于我们在第十三章,2)泛型中写了一个功能,使用了两个不同类型的Vector
,进行比较大小报错的问题,我们现在能解决了
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上的数据进行复制的操作
引用类型比较
那么我们想要让引用类型进行比较呢,这样就行了
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 的类型实现方法。
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());
}
输出:
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
方法
fn main() {let s = 3.to_string();
}
十四、生命周期
- Rust的每个引用都有自己的生命周期
- 生命周期:让引用保持有效的作用域
- 大多数情况下:生命周期是隐式的、可被推断的
- 当引用的生命周期可能以不同的方式互相关联时:手动标注生命周期
1)避免悬垂引用
- 生命周期的主要目标:避免悬垂引用(dangling reference)
- 运行下面的代码
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
:考虑引入命名的生命周期参数像下面那样
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
报错解释:
要通过'a
这种方式,告诉借用检查器,传入的x
和y
跟返回的&str
生命周期是相同的,因为函数是不能知道它引用的参数到底是什么情况,说不定已经失效了呢,防止这种现象的发生
修改后的代码
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}
}
输出:
The longest string is abcd
代码说明:
在下面4)生命周期标注
中会谈到
4)生命周期标注
- 生命周期的标注:描述了多个引用的生命周期的关系,但不影响生命周期,
- 我们并没有改变任何传入和返回的值的生命周期。而是指出任何不遵守这个协议的传入值都将被借用检查器拒绝。
语法
- 生命周期参数名:
- 以
'
为开头 - 通常全部小写而且很短
- 比如说经常使用的
'a
- 生命周期标注的位置
- 在引用
&
符号后面 - 使用空格将标注和引用类型分开
例子
&i32
一个引用&'a i32
带有显式生命周期的引用&'a mut i32
带有显式生命周期的可变引用
5)在函数签名中的生命周期标注
- 泛型生命周期参数声明在:函数名和参数列表之间的
<>
里 - 就像上面的例子那样
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
的生命周期不同会怎样
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
的生命周期再长一些
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)深入理解生命周期
指定生命周期参数的方式依赖于函数所做的事情
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}
}
输出:
The longest string is abcd
我们改一下代码第10-14
行,让返回值只返回一个参数,也就是y
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
}
输出:
The longest string is xyz
现在这个函数longest
返回的值生命周期就和y
有关,那我们也就可以把代码第9
行,关于指定x
的生命周期'a
删掉了
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
}
从函数返回引用不指向任何参数
- 从函数返回引用时,返回类型的生命周期参数需要与其中一个参数的生命周期匹配
- 如果返回的引用没有指向任何参数,那么它只能引用函数内创建的值
- 这很容易出现悬垂引用,指向的内存已经被清理掉了
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
就完了
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
字段是引用类型,需要添加生命周期标注 - 看下面的例子
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,};
}
输出:
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
,在第七章 重新温故一下,
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 中,这的确是不能编译的。每一个引用都必须有明确的生命周期。那时的函数签名将会写成这样:
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
函数签名中的引用的生命周期
开始时签名中的引用并没有关联任何生命周期:
fn first_word(s: &str) -> &str {
接着编译器应用第一条规则,也就是每个引用参数都有其自己的生命周期。我们像往常一样称之为 'a
,所以现在签名看起来像这样:
fn first_word<'a>(s: &'a str) -> &str {
对于第二条规则,因为这里正好只有一个输入生命周期参数所以是适用的。第二条规则表明输入参数的生命周期将被赋予输出生命周期参数,所以现在签名看起来像这样:
fn first_word<'a>(s: &'a str) -> &'a str {
现在这个函数签名中的所有引用都有了生命周期,如此编译器可以继续它的分析而无须程序员显式的标记这个函数签名中的生命周期。
例子2
再次假设自己是编译器
fn longest(x: &str, y: &str) -> &str {
应用第一条规则:每个引用参数都有其自己的生命周期。这次有两个参数,所以就有两个(不同的)生命周期:
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 {
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"));
}
输出:
1
2
Attention please: 3
2
代码说明:
第1-3
行:声明一个内部有引用类型的struct
,如果有引用类型就必须标注生命周期
第5-9
行:实现了ImportantExcerpt
,创建了一个方法level
,因为返回值是i32
,不是引用不涉及生命周期,因为生命周期存在的意义就是,如果你用到一个返回值,而这个返回值出现悬垂引用的现象,现在返回值一直存在,所以代码第6
行,fn level(&self)
不必写为fn level(&'a self)
第11-15
行:
按编译器应用第一条规则,每个输入生命周期的引用参数都有其自己的生命周期
fn announce_and_return_part1(&'a self) -> &str
按编译器应用第二条规则,只有 1 个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数
fn announce_and_return_part1(&'a self) -> &'a str
所有的引用生命周期都能确定,那么就可以省略,所以可以写为这样
fn announce_and_return_part1(&self) -> &str {
第17-22
行:
按编译器应用第一条规则,每个输入生命周期的引用参数都有其自己的生命周期
fn announce_and_return_part2(&'a self, announcement: &'a str) -> &str {
按编译器应用第三条规则,如果有多个输入生命周期参数,但其中一个是&self
或&mut self
,那么 self
的生命周期会被赋给所有输出生命周期参数
fn announce_and_return_part2(&'a self, announcement: &'a str) -> &'a str {
所有的引用生命周期都能确定,那么就可以省略,所以可以写为这样
fn announce_and_return_part2(&self, announcement: &str) -> &str {
9)静态生命周期
'static
是一个特殊的生命周期,表示:整个程序的执行期- 所有的字符串字面值都拥有
'static
生命周期,我们也可以选择像下面这样标注出来: - 这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是
'static
的。
fn main() {
let s: &'static str = "I have a static lifetime.";
}
你可能在错误信息的帮助文本中见过使用 'static
生命周期的建议,不过将引用指定为 'static
之前,思考一下这个引用是否真的在整个程序的生命周期里都有效。你可能会考虑希望它一直有效,如果可能的话。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个 'static
的生命周期。
10)结合泛型类型参数、trait bounds 和生命周期
use std::fmt::Display;fn ptn<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
whereT: Display,
{println!("Announcement! {}", ann);if x.len() > y.len() {x} else {y}
}
fn main() {println!("{}", ptn("abc", "ab", "ac"))
}
输出:
Announcement! ac
abc
代码说明:
第3-5
行:一个叫ptn
的方法,传入的参数,x
,y
,和一个实现了Display
这个trait
参数T
,返回了一个&str
为什么不忽略'a
按编译器应用第一条规则,每个输入生命周期的引用参数都有其自己的生命周期
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
函数签名中的引用的生命周期
开始时签名中的引用并没有关联任何生命周期:
fn first_word(s: &str) -> &str {
接着编译器应用第一条规则,也就是每个引用参数都有其自己的生命周期。我们像往常一样称之为 'a
,所以现在签名看起来像这样:
fn first_word<'a>(s: &'a str) -> &str {
对于第二条规则,因为这里正好只有一个输入生命周期参数所以是适用的。第二条规则表明输入参数的生命周期将被赋予输出生命周期参数,所以现在签名看起来像这样:
fn first_word<'a>(s: &'a str) -> &'a str {
现在这个函数签名中的所有引用都有了生命周期,如此编译器可以继续它的分析而无须程序员显式的标记这个函数签名中的生命周期。
例子2
再次假设自己是编译器
fn longest(x: &str, y: &str) -> &str {
应用第一条规则:每个引用参数都有其自己的生命周期。这次有两个参数,所以就有两个(不同的)生命周期:
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 {
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"));
}
输出:
1
2
Attention please: 3
2
代码说明:
第1-3
行:声明一个内部有引用类型的struct
,如果有引用类型就必须标注生命周期
第5-9
行:实现了ImportantExcerpt
,创建了一个方法level
,因为返回值是i32
,不是引用不涉及生命周期,因为生命周期存在的意义就是,如果你用到一个返回值,而这个返回值出现悬垂引用的现象,现在返回值一直存在,所以代码第6
行,fn level(&self)
不必写为fn level(&'a self)
第11-15
行:
按编译器应用第一条规则,每个输入生命周期的引用参数都有其自己的生命周期
fn announce_and_return_part1(&'a self) -> &str
按编译器应用第二条规则,只有 1 个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数
fn announce_and_return_part1(&'a self) -> &'a str
所有的引用生命周期都能确定,那么就可以省略,所以可以写为这样
fn announce_and_return_part1(&self) -> &str {
第17-22
行:
按编译器应用第一条规则,每个输入生命周期的引用参数都有其自己的生命周期
fn announce_and_return_part2(&'a self, announcement: &'a str) -> &str {
按编译器应用第三条规则,如果有多个输入生命周期参数,但其中一个是&self
或&mut self
,那么 self
的生命周期会被赋给所有输出生命周期参数
fn announce_and_return_part2(&'a self, announcement: &'a str) -> &'a str {
所有的引用生命周期都能确定,那么就可以省略,所以可以写为这样
fn announce_and_return_part2(&self, announcement: &str) -> &str {
9)静态生命周期
'static
是一个特殊的生命周期,表示:整个程序的执行期- 所有的字符串字面值都拥有
'static
生命周期,我们也可以选择像下面这样标注出来: - 这个字符串的文本被直接储存在程序的二进制文件中而这个文件总是可用的。因此所有的字符串字面值都是
'static
的。
fn main() {
let s: &'static str = "I have a static lifetime.";
}
你可能在错误信息的帮助文本中见过使用 'static
生命周期的建议,不过将引用指定为 'static
之前,思考一下这个引用是否真的在整个程序的生命周期里都有效。你可能会考虑希望它一直有效,如果可能的话。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个 'static
的生命周期。
10)结合泛型类型参数、trait bounds 和生命周期
use std::fmt::Display;fn ptn<'a, T>(x: &'a str, y: &'a str, ann: T) -> &'a str
whereT: Display,
{println!("Announcement! {}", ann);if x.len() > y.len() {x} else {y}
}
fn main() {println!("{}", ptn("abc", "ab", "ac"))
}
输出:
Announcement! ac
abc
代码说明:
第3-5
行:一个叫ptn
的方法,传入的参数,x
,y
,和一个实现了Display
这个trait
参数T
,返回了一个&str
为什么不忽略'a
按编译器应用第一条规则,每个输入生命周期的引用参数都有其自己的生命周期
fn ptn<T>(x: &'a str, y: &'a str, ann: T) -> &str
编译器无法应用第二条规则,因为有多个参数
编译器无法应用第三条规则,有多个参数但是没有self
所以在编译器眼中,返回参数是&str
,没有生命周期,所以你必须要显式的标注生命周期
然后这个方法还有个参数还有个T
,函数声明就要写成<'a, T>
这样
相关文章:
【2025rust笔记】超详细,小白,rust基本语法
一、常见cargo命令 查看cargo版本 cargo --version创建cargo项目 create new demo_name构建编译项目 cargo build运行项目 cargo run检查项目代码 cargo check (比cargobuild快)发布构建项目 cargo build --release 电子markdown/pdf格式 二、小demo-----猜数游戏 1、print…...
将 SSH 密钥添加到 macOS 的钥匙串中
git提交代码时,如果SSH密码并未免密,每次拉取,上传操作时都需要密码输入, 可将SSH密钥添加到钥匙串中 git config --global credential.helper store报错: WARNING: The -K and -A flags are deprecated and have bee…...
Gpt翻译完整版
上一篇文章收到了很多小伙伴的反馈,总结了一下主要以下几点: 1. 说不知道怎么调api 2. 目前只是把所有的中文变成了英文,如果想要做多语言还需要把这些关键字提炼出来成放到message_zh.properties和message_en.properties文件中,…...
CC++的内存管理
目录 1、C/C内存划分 C语言的动态内存管理 malloc calloc realloc free C的动态内存管理 new和delete operator new函数和operator delete函数 new和delete的原理 new T[N]原理 delete[]的原理 1、C/C内存划分 1、栈:存有非静态局部变量、函数参数、返回…...
HTTP 状态代码 501 502 问题
问题 单个客户端有时会出现 报错 501 或 502 如下: System.Net.Http.HttpRequestException: Response status code does not indicate success: 501 (Not Implemented) 分析 可以排除 服务器无法处理的问题(测试发现 一个客户端报错,不会影响…...
virtio_video virtio_snd
在 Qualcomm 平台的 虚拟机(VM) 环境中,qcom,virtio_snd 是 VirtIO 机制下的 音频 DMA 共享 设备节点,它用于 虚拟机和宿主机(Hypervisor)之间共享音频数据,类似于标准的 VirtIO 声音设备。 1️…...
【大模型安全】大模型安全概述
【大模型安全】大模型安全概述 1.大模型安全目前的关键挑战技术安全合规安全 2.大语言模型的安全隐患与主要风险点3.大语言模型与国家安全风险4.大语言模型的信息安全原则 1.大模型安全目前的关键挑战 技术安全 1、数据的安全与合理利用 大语言模型通常需要处理大量敏感数据…...
基于 vLLM 部署 LSTM 时序预测模型的“下饭”(智能告警预测与根因分析部署)指南
Alright,各位看官老爷们,准备好迎接史上最爆笑、最通俗易懂的 “基于 vLLM 部署 LSTM 时序预测模型的智能告警预测与根因分析部署指南” 吗? 保证让你笑出猪叫,看完直接变身技术大咖!🚀😂 咱们今天的主题,就像是要打造一个“智能运维小管家”! 这个小管家,不仅能提…...
深入理解三色标记、CMS、G1垃圾回收器
三色标记算法 简介 三色标记算法是一种常见的垃圾收集的标记算法,属于根可达算法的一个分支,垃圾收集器CMS,G1在标记垃圾过程中就使用该算法 三色标记法(Tri-color Marking)是垃圾回收中用于并发标记存活对象的核心算…...
【车规芯片】如何引导时钟树生长方向
12nm车规DFTAPR项目中,我们可以看到,绝大部分的sink都受控于xxxx_tessent_occ_clk_cpu_inst/tessent_persistent_cell_clock_out_mux/C10_ctmi_1这个mux,这是我们DFT设计结果: 这里我们重新打开place的数据 Anchor,也就…...
基于Spring Boot的企业车辆管理系统设计与实现(LW+源码+讲解)
专注于大学生项目实战开发,讲解,毕业答疑辅导,欢迎高校老师/同行前辈交流合作✌。 技术范围:SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:…...
Spring Boot 中短时间连续请求时出现Cookie获取异常问题
Spring Boot 中短时间连续请求时出现Cookie获取异常问题 一、问题描述:异步线程操作导致请求复用时 Cookie 解析失败1. 场景背景2. 问题根源 二、问题详细分析1. 场景重现2. 问题分析 三、如何避免影响下一次请求?✅方式 1:在主线程提前复制 …...
轮播图案例
(1)、搭建轮播图的结构 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><title>轮播图结构</title><!-- <script src"../js/tools.js"></script> --…...
通俗版解释:分布式和微服务就像开餐厅
一、分布式系统:把大厨房拆成多个小厨房 想象你开了一家超火爆的餐厅,但原来的厨房太小了: 问题:一个厨师要同时切菜、炒菜、烤面包,手忙脚乱还容易出错。 解决方案: 拆分成多个小厨房(分布式…...
【开源-常用C/C++命令行解析库对比】
以下是几种常用的C/C命令行解析库的对比表格,以及它们的GitHub开源库地址: 库名称语言特点是否支持子命令是否支持配置文件是否支持自动生成帮助信息GitHub地址ClaraC11及以上单一头文件,轻量级,非异常错误处理,自动类…...
JavaEE_多线程(一)
目录 1. 为啥要有线程1.1 线程是什么1.2 进程和线程的区别1.3 Java如何进行多线程编程 2 使用线程2.1 创建线程2.2 Thread类的几个常见方法和属性2.2.1 Thread常见构造方法2.2.2 Thread常见属性2.2.3 常见其他方法 2.3 终止一个线程2.3.1 通过共享的标记位来进行沟通2.3.2 调用…...
table 拖拽移动
表格拖拽 Sortable.js中文网|配置 <!-- 教务处 --><template><div class"but"><el-button click"mergeAndPrintArrays()" type"primary">保存数据</el-button><el-button click"restoration()" t…...
【QGIS二次开发】地图显示与交互-01
1. 系统界面设计 设计的系统界面如下,很好还原了QGIS、ArcGIS等软件的系统界面,充分利用了QT中顶部工具栏、菜单栏、底部状态栏,实现了图层管理器、鹰眼图、工具箱三个工具面板。 菜单栏、工具栏、工具箱集成了系统中实现的全部功能&#x…...
Microsoft.Office.Interop.Excel 的简单操作
Microsoft.Office.Interop.Excel 的简单操作 1、安装 Microsoft.Office.Interop.Excel2、声明引用 Microsoft.Office.Interop.Excel3、简单的新建 EXCEL 操作代码4、将 DataGridView 表数据写到 EXCEL 操作代码5、将 EXCEL 表数据读取到 C# 数据表 DataTable 操作代码 1、安装 …...
Debezium日常分享系列之:Debezium 3.0.8.Final发布
Debezium日常分享系列之:Debezium 3.0.8.Final发布 稀疏向量逻辑类型重命名架构历史配置默认值的更改潜在的 Vitess 数据丢失Oracle 的 Reselect 列后处理器行为更改MariaDB 的 SSL 连接 稀疏向量逻辑类型重命名 PostgreSQL 扩展 vector(也称为 pgvecto…...
论传输层的TCP协议和UDP协议scoket通讯
TCP传输前需要三次握手---断开需要四次挥手 谁先发出请求、谁是客户端(client)另一个就是服务器(server)---简称CS架构 现在更多的是BS架构 什么是BS架构? blower server 浏览器服务器---也就是浏览器代替了客户端…...
《鸢尾花数学大系:从加减乘除到机器学习》开源资源
《鸢尾花数学大系:从加减乘除到机器学习》开源资源 Gitee:https://gitee.com/higkoo/ bilibili:https://space.bilibili.com/513194466 GitHub:https://github.com/Visualize-ML...
DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)示例2: 分页和排序
前言:哈喽,大家好,今天给大家分享一篇文章!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)示例2: 分页和排序📚页面效果📚指令输入定义…...
MySQL——DQL、多表设计
目录 一、DQL 1.基本查询 2.条件查询 3.分组查询 4.排序查询 5.分页查询 二、多表设计 1.一对多 2.一对一 3.多对多 一、DQL 1.基本查询 注意: *号代表查询所有字段,在实际开发中尽量少用(不直观、影响效率) 2.条件查询…...
【文献阅读】The Efficiency Spectrum of Large Language Models: An Algorithmic Survey
这篇文章发表于2024年4月 摘要 大语言模型(LLMs)的快速发展推动了多个领域的变革,重塑了通用人工智能的格局。然而,这些模型不断增长的计算和内存需求带来了巨大挑战,阻碍了学术研究和实际应用。为解决这些问题&…...
玩转大模型——Trae AI IDE国内版使用教程
文章目录 Trae AI IDE完备的 IDE 功能强大的 AI 助手 安装 Trae 并完成初始设置管理项目什么是 “工作空间”?创建项目 管理插件安装插件从 Trae 的插件市场安装从 VS Code 的插件市场安装 禁用插件卸载插件插件常见问题暂不支持安装 VS Code 插件市场中某个版本的插…...
【实战 ES】实战 Elasticsearch:快速上手与深度实践-2.3.1 避免频繁更新(Update by Query的代价)
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 Elasticsearch数据更新与删除深度解析:2.3.1 避免频繁更新(Update by Query的代价)案例背景1. Update by Query的内部机制解析1.1 文档更…...
stable-diffusion-webui 加载模型文件
背景 stable-diffusion-webui 安装完毕后,默认的模型生成的效果图并不理想,可以根据具体需求加载指定的模型文件。国内 modelscope 下载速度较快,以该站为例进行介绍 操作步骤 找到指定的模型文件 在 https://modelscope.cn/models 中查找…...
BKA-CNN基于黑翅鸢算法优化卷积神经网络的数据多特征分类预测Matlab
BKA-CNN基于黑翅鸢算法优化卷积神经网络的数据多特征分类预测Matlab 目录 BKA-CNN基于黑翅鸢算法优化卷积神经网络的数据多特征分类预测Matlab分类效果基本介绍BKA-CNN基于黑翅鸢算法优化卷积神经网络的数据多特征分类预测一、引言1.1、研究背景和意义1.2、研究现状1.3、研究目…...
SparkStreaming之04:调优
SparkStreaming调优 一 、要点 4.1 SparkStreaming运行原理 深入理解 4.2 调优策略 4.2.1 调整BlockReceiver的数量 案例演示: object MultiReceiverNetworkWordCount {def main(args: Array[String]) {val sparkConf new SparkConf().setAppName("Networ…...
maven高级-05.私服
一.私服...
FFmpeg-chapter2-C++中的线程
1 常规的线程 一般常规的线程如下所示 // CMakeProject1.cpp: 定义应用程序的入口点。 //#include "CMakeProject1.h" #include <thread> using namespace std;void threadFunction(int index) {for (int i 0; i < 1000; i){std::cout << "Th…...
【前端】简单原生实例合集html,css,js
长期补充,建议关注收藏点赞。 目录 a标签设置不一样的花样(图片但不用img)侧边固定box分栏input各种类型iframe表单拖拽 a标签设置不一样的花样(图片但不用img) a标签里面不用嵌套img,直接设置为其bg-img即可 <!DOCTYPE html…...
Linux下的shell指令(一)
作业 1> 在终端提示输入一个成绩,通过shell判断该成绩的等级 [90,100] : A [80, 90) : B [70, 80) : C [60, 70) : D [0, 60) : 不及格 #!/bin/bash read -p "请输入学生成绩:" score if [ "$score" -ge 90 ] && [ "$scor…...
AJAX介绍
XMLHttpRequest get请求使用 const xhr new XMLHttpRequest(); xhr.open("GET", "/data/test.json", true); xhr.onreadystatechange function () {if (xhr.readyState 4) {if (xhr.status 200) {alert(xhr.responseText);} else {console.log("…...
Serilog: 强大的 .NET 日志库
Serilog 是一个功能强大的日志记录库,专为 .NET 平台设计。它提供了丰富的 API 和可插拔的输出器及格式化器,使得开发者能够轻松定制和扩展日志记录功能。在本文中,我们将探索 Serilog 的基础知识、API 使用、配置和一些常见的示例。 1. 日志…...
串口通讯基础
第1章 串口的发送和接收过程 1.1 串口接收过程 当上位机给串口发送(0x55)数据时,MCU的RX引脚接受到(0x55)数据,数据(0x55)首先进入移位寄存器。数据全部进入移位寄存器后,一次将(0x55)全部搬运…...
SSL证书和HTTPS:全面解析它们的功能与重要性
每当我们在互联网上输入个人信息、进行在线交易时,背后是否有一个安全的保障?这时,SSL证书和HTTPS便扮演了至关重要的角色。本文将全面分析SSL证书和HTTPS的含义、功能、重要性以及它们在网络安全中的作用。 一、SSL证书的定义与基本概念 S…...
全国青少年航天创新大赛各项目对比分析
全国青少年航天创新大赛各项目对比分析 一、比赛场地对比 项目名称场地尺寸场地特点组别差异筑梦天宫虚拟三维场景动态布局,小学组3停泊处,初高中组6停泊处;涉及传送带、机械臂、传感器等虚拟设备。初中/高中组任务复杂度更高,运…...
低空监视-无人机专用ADS-B应答机
产品简介 ping200XR是经过TSO适航认证的无人机专用ADS-B应答机,用于中大型无人机的低空监视。将经过认证的航空级航电设备引入无人机系统。该应答机支持航管二次雷达A,C/S模式和ADS-B OUT。重量仅52克满足无人机所面临的尺寸、重量、功耗的挑战…...
【linux】文件与目录命令 - sort
文章目录 1. 基本用法2. 常用参数3. 用法举例4. 注意事项 sort 命令用于对文本文件中的行进行排序,支持按字母顺序、数值大小、特定字段等方式进行排序。默认按字母顺序升序排序。 1. 基本用法 语法: sort [选项] 文件 sort [选项] -o 输出文件 文件功能…...
【工具推荐】在线提取PDF、文档、图片、论文中的公式
网址1:https://simpletex.cn/ai/latex_ocr 网址2: https://www.latexlive.com/home 推荐理由:无需下载,在线使用,直接 截图 CTRLV 效果更佳。...
大模型在垂直行业的落地实践:从通用到定制化的技术跃迁
大模型在垂直行业的落地实践:从通用到定制化的技术跃迁 一、通用大模型的局限性:从 “全能” 到 “专精” 的转型挑战 通用大模型,如 GPT 系列,凭借其强大的自然语言处理能力,在文本生成、语义理解、机器翻译等基础任…...
FPGA开发,使用Deepseek V3还是R1(1):应用场景
以下都是Deepseek生成的答案 FPGA开发,使用Deepseek V3还是R1(1):应用场景 FPGA开发,使用Deepseek V3还是R1(2):V3和R1的区别 FPGA开发,使用Deepseek V3还是R1&#x…...
Docker概念与架构
文章目录 概念docker与虚拟机的差异docker的作用docker容器虚拟化 与 传统虚拟机比较 Docker 架构 概念 Docker 是一个开源的应用容器引擎。诞生于 2013 年初,基于 Go 语言实现。Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中…...
算法之数据结构
目录 数据结构 数据结构与算法面试题 数据结构 《倚天村 • 图解数据结构》 | 小傅哥 bugstack 虫洞栈 ♥数据结构基础知识体系详解♥ | Java 全栈知识体系 线性数据结构 | JavaGuide 数据结构与算法面试题 数据结构与算法面试题 | 小林coding...
Mysql学习笔记(六)Django连接MySQL
一、Django中的MySQL驱动程序 Python中常见的MySQL驱动程序: MySQL-python:就是MySQLdb,是对C语言操作MySQL的封装,支持Python2,不支持Python3mysqlclient:MySQL-python的另一个分支,支持Python3pymysql&am…...
手机号码归属地的实现
手机号码归属地查询一般可以通过以下几种方式实现: 1. 使用公开的号码归属地数据库 可以使用国内的手机号码归属地数据库,如: 百度号码归属地开放API阿里云号码归属地API腾讯号码归属地API 你可以在本地存储一个 CSV 或 SQLite 数据库&…...
本地部署 DeepSeek:从 Ollama 配置到 Spring Boot 集成
前言 随着人工智能技术的迅猛发展,越来越多的开发者希望在本地环境中部署和调用 AI 模型,以满足特定的业务需求。本文将详细介绍如何在本地环境中使用 Ollama 配置 DeepSeek 模型,并在 IntelliJ IDEA 中创建一个 Spring Boot 项目来调用该模型…...
2025年渗透测试面试题总结- 阿某云安全实习(题目+回答)
网络安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 阿里云安全实习 一、代码审计经验与思路 二、越权漏洞原理与审计要点 三、SSRF漏洞解析与防御 四、教…...