前言
弱小和无知不是生存的障碍,傲慢才是
参考:
Rust 程序设计语言:https://rust.bootcss.com/
Rust程序设计语言重编版:http://shouce.jb51.net/rust-book-chinese/index.html
Runoob—Rust教程:https://www.runoob.com/rust/rust-tutorial.html
B站视频—Rust编程语言入门教程—杨旭:https://www.bilibili.com/video/BV1hp4y1k7SV?share_source=copy_web
一、Rust安装
边看B站视频边看笔记效果更佳
https://www.bilibili.com/video/BV1hp4y1k7SV?share_source=copy_web
1.2
节介绍了安装Rust
Windows + R cmd
rustc --version
二、使用Cargo创建项目
1、查看Cargo版本
Windows + R cmd
cargo --version
2、创建Cargo项目
cargo new hello_cargo[项目名]
3、构建(编译)Cargo项目
cargo build
4、构建(编译)并运行Cargo项目
cargo run
先编译后运行,如果编译过了,直接运行可执行二进制文件
修改源文件会重新编译
该二进制文件会在这个目录下
hello_cargo[项目名]\target\debug\hello_cargo.exe
5、检查代码
cargo check
-
cargo check,检查代码确保能通过编译,但是不产生任何可执行文件
-
cargo check 要比 cargo build 快很多
编写代码可以连续反复使用cargo check 检查代码,提高效率
6、为发布构建
cargo build --release
- 编译时会进行优化
- 代码会运行的更快,但编译时间更长
- 会在 target\release 而不是 target\debug 下生成可执行文件
两种构建方式一个为了开发,一个为了发布
三、猜数游戏
1、输入一个数能打印出来
1)前置知识
声明变量
mut 将 “不可变值” 变为 “可变值”
将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 ,所有的声明都要有初始值
读取输入
io::stdin().read_line(&mut guess).expect("无法读取行");
io
下的stdin()
下的read_line()
方法:将用户一行输入放到字符串中,也就是 guess 中
read_line()
会返回io::Result<usize>
Result这个枚举类型中有两个"变体"(枚举类型中的固定值称作变体)
Ok 和 Err
返回的是 Ok,表示操作成功了,附加成功的字符串
如果返回的是 Err,表示操作失败了,附加失败的字符串以及报错信息
然后调用.expect()
方法
如果read_line()
返回的io::Result
实例的值是Ok,那么expect()
会提取Ok的附加值,作为结果返回给用户
如果read_line()
返回的io::Result
实例的值是Err,那么expect()
会把程序中断,并把传入的字符串信息显示出来
输出
println!("你猜测的数是{}",guess);
{ } 占位符
2)代码
use std::io;
fn main() {
println!("猜数");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("无法读取行");
println!("你猜测的数是{}",guess);
}
2、生成一个1~100之间的随机数
1)前置知识
导入生成随机数的包
Rust中的库 叫做 crate
官方的库
https://crates.io/
在Cargo.toml 文件中引入
[dependencies]
rand = "^0.3.14"
^表示任何与这个版本公共API兼容的版本都可以
启动这个包
在VsCode中,引入包后运行这个命令会帮你构建一些
Ctrl + Shift + P
>rust
Rust: Start the Rust server
更改引入包的版本
如果Cargo.toml
文件引入的
[dependencies]
rand = "0.3.14"
那么在你第一次构建的时候
会在Cargo.lock
这个文件写入所有依赖项的版本
并且会写入最新版本
比如说 在Cargo.toml
引入的版本rand = "0.3.14"
那么会在Cargo.lock
写入0.3版本的的最新小版本
[[package]]
name = "guess_number"
version = "0.1.0"
dependencies = [
"rand 0.3.23",
]
再次构建的时候就会先查看Cargo.lock
这个文件里面的版本
在代码中引入包
use rand::Rng;
这个包是一个trait
你可以把它想成接口
而在代码中引入包相当于一个类实现一个接口
所以如果你引入,而不用就会报错
gen_range()
方法
gen_range(1,101)
会在 [ 1 , 101 )之间生成随机数,1-100之间包含1和100(左闭右开 )
2)代码
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、比较生成的随机数和输入数字
1)前置知识
引入Ordering
Ordering有三个变体Less
、Greater
、Equal
两个数进行比较
guess.cmp(&secret_number)
是用 guess 和 secret_number 进行比较,这个方法会返回一个Ordering
match
表达式,可以让我们根据cmp()
方法返回的Ordering的值来决定我们下一步做什么
一个match
表达式是由多个手臂(arm)或者叫分支组成的,有点像switch
每个手臂含有一个用于匹配的模式这里就是Less
、Greater
、Equal
如果match
后面紧跟着的值与某一个 arm 的模式匹配,那么就会执行这个 arm 里面的代码
match
是按从上到下的顺序进行匹配的
类型转换
guess.cmp(&secret_number)
中的 guess 你输入的是字符串,而 secret_number 是随机的整数,进行比较会报错
因此要把 guess 变为整数进行比较
let guess: u32 = guess.trim().parse().expect("请输入整数");
guess.trim()
会删除空格、tab、以及回车(输入时候按回车就会有 \n 输入)
parse()
会把输入的字符串解析,有i32
u32
i64
,我们这里选择解析成u32
let guess: u32
在变量后面加 : u32
2)代码
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);
let guess: u32 = guess.trim().parse().expect("请输入整数");
match guess.cmp(&secret_number){
Ordering::Less => println!("太小了"),
Ordering::Greater => println!("太大了"),
Ordering::Equal => println!("猜对了"),
}
}
Ctrl+左键 查看示例注意:
把下面的代码注释掉,默认是i32
当把 guess 字符串解析为u32
时,secret_number也变为了u32
4、多次猜测(终版)
1)前置知识
设置无限循环
loop{}
结束循环
break;
结束全部循环
continue;
结束本次循环
如何输入字符串不会中止程序
原本的程序
let guess: u32 = guess.trim().parse().expect("请输入整数");
如果输入字符串,会直接抛出异常中止程序
我们想要遇到异常,不结束程序,并且告诉他输入数字
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("输入数字!");
continue;
}
};
guess.trim().parse()
的返回值是 Result
Result 有两个变体 Ok 和 Err
Ok 的情况下,正常赋值
Err 的情况下,告诉用户输入数字,并且结束本次循环(然后继续从头走循环)
2)代码
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
关键字 - 默认情况下,变量是不可变的(immutable)
- 声明变量时,在变量前加上
mut
,就可使变量可变 Ctrl+左键 查看示例
1)变量与常量
常量(constant),常量在绑定值以后也是不可变的,但它与不可变的变量有很多区别
-
不可以使用 mut ,常量永远都是不可变的
-
声明常量使用 const 关键字,它的类型必须被标注
-
常量可以在任何作用域内进行声明,包括全局作用域
-
在程序运行期间,常量在其声明的作用域内一直有效,因此可以作为不同代码之间共享值
const MAX_POINTS: u32 = 10_0000; fn main() { const MAX_POINTS: u32 = 10_0000; }
-
常量只可以绑定到常量表达式,无法绑定到函数的调用结果或只能在运行时才能计算的值(就是在编译期就确定了)
命名规范:Rust 里常量使用全大写字母,每个单词之间用下划线分开,例如:
const MAX_POINTS: u32 = 10_0000
数字增加下划线只是为了增加可读性
2)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);
}
但是shadow 和把变量标记为
mut
是不一样的
使用let
声明的同名新变量,也是不可变的
使用let
声明的同名新变量,它的类型可以与之前不同
shadow :
fn main() {
let str = " ";
let str = str.len();
println!("长度{}",str);
}
mut
:
fn main() {
let mut str = " ";
str = str.len();//报错:把一个整数赋给字符串
println!("长度{}",str);
}
2、数据类型
Rust 是静态编译语言,在编译时必须知道所有变量的类型
-
基于使用的值,编译器通常能够推断出它的具体类型 Ctrl+左键 查看示例
-
但如果可能的值比较多(例如把
String
转成整数的parse()
方法),就必须添加类型的标注,否则编译会报错let str: u32 = "2".parse().expect("Not a number");
1)标量类型
一个标量类型代表一个单个的值
Rust 有四个主要的标量类型:
整数类型
- 整数类型没有小数部分
- 例如
u32
就是一个无符号的整数类型,占据 32 位的空间 - 无符号整数类型以 u 开头(无符号指的是,非负)
- 有符号整数类型以 i 开头
i:有符号整数范围
[
−
(
2
n
−
1
)
,
2
n
−
1
−
1
]
[-(2^n - 1) , 2^{n-1} - 1]
[−(2n−1),2n−1−1]
u:无符号整数范围
[ 0 , 2 n − 1 − 1 ] [0,2^{n-1} - 1] [0,2n−1−1]
如表格所示,每种都分 i 和 u 以及固定的位数
Length | Signed | Unsigned |
---|---|---|
8-bit | i8 | u8 |
16-bit | i16 | u16 |
32-bit | i32 | u32 |
64-bit | i64 | u64 |
128-bit | i128 | u128 |
arch | isize | usize |
表格最后的 isize 和 usize类型
isize 和 usize类型的位数由程序运行的计算机的架构决定:
如果是 64 位计算机,那就是 64 位
…
使用 isize 和 usize 的主要场景是对某种集合进行索引操作(不常见)
整型字面值
为了便于辨识整型字面值,可以在字面值中加入 _ 用作数字之间的分隔,如:1_000_000,_ 所起的作用仅仅是方面代码的阅读,它与1000000表示的相同的数值;另外除了字节字面值(即以0,1序列表示的数值),其他类型的整型字面值都可以加上类型说明符作为后缀以标识数值类型,如:255u8
, 1024i64
或者1024_i64
等。
let a = 255u8
一个数字,可以采用不同的进制表示,如十进制,十六进制,八进制和是二进制等。区分这些不同进制的数的方式是根据字面值所带的前缀,如下表:
字面值类型 | 示例 |
---|---|
二进制 | 0b100_0001 |
八进制 | 0o101 //零欧 |
十进制 | 65 |
十六进制 | 0x41 |
字节(只能是u8) | b’A’ |
如果你不太清楚使用哪种数据类型,整数默认类型就是i32
整数溢出
例如:u8
的范围是 0 ~ 255,如果你把一个u8
变量的值设称 256,就会发生整数溢出:
这分为两种情况
-
调试模式下编译:Rust 会检查整数溢出,如果发生溢出,程序在运行时就会panic(恐慌)
-
发布模式下(–release)编译:Rust 不会检查可能导致 panic 的整数溢出
如果溢出发生:Rust 会执行“环绕”操作
256变为0,257变为1
但程序不会 panic浮点类型
浮点类型
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;
字符类型
-
Rust 语言中
char
类型被用来描述语言中最基础的单个字符 -
字符类型的字面值使用单引号
-
占用 4 个字节大小 (1个字节 = 1 byte = 8 位 = 8 bit 比特 = 8 个 0,1)
-
是 Unicode 标量值,可以表示比 ASCⅡ 多得多的字符内容:拼音,中日韩文,零长度空白字符,emoji表情等
范围:
U+0000 ~ U+D7FF
U+E000 ~ U+10FFFF
-
但 Unicode 中并没有 “字符” 的概念,所以直觉上认为的字符也许与 Rust 中的概念并不相符
请注意字符类型是单引号
如果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');//创建Tuple
println!("{},{},{}",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];
数组的用处
-
如果想让你的数据存放在 stack(栈)上而不是 heap(堆)上,或者想保证有固定数量的元素,这时使用数组更有好处
-
数组没有 Vector 灵活
- Vector 和数组类似,它由标准库提供
- Vector 的长度可以改变
- 如果不确定用数组还是 Vector,那大概率是Vector
数组的声明
- 数组的声明以这种形式表示 [ 类型;长度 ]
let a:[u32; 2];
a = [1,2];
-
一种特殊的数组声明
如果数组的每个元素值都相同,那么可以这样
请注意,声明中间的是分号
let a = [3; 5];//它就相当于 let a = [3, 3, 3, 3, 3];
访问数组
-
跟其他语言一样,
a[0]
表示访问 a 数组的第一个 -
如果访问的索引超出了数组的范围,简单一些的 Rust 在
build
时候能检测出来,绕一些
build
就检测不出来,但是cargo run
的时候会报错
简单一些的逻辑
build
能检测出来
复杂一些的逻辑
build
就不会检测出来
let a = [1,2,3,4];
let index = [0,5,7];
println!("{}", a[index[2]]);
但是运行会报错
3、函数和注释
1)函数
-
声明函数使用
fn
关键字 -
依照惯例,针对函数和变量名,Rust 使用 snake case 命名规范
- 所有的字母都是小写,单词之间用下划线隔开
- 例子
fn main() {
println!("hello word!");
another_function();//调用方式
}
fn another_function() {
println!("another function");
}
函数的参数
如果函数有参数,必须声明每个参数的类型
fn main() {
another_function(5, 6);
}
fn another_function(x: i32, y: i32) {
println!("the value of x is : {}, y is : {}", x, y);
}
函数体中的语句和表达式
- 函数体由一系列语句组成,可选的由一个表达式结束
- Rust 是一个基于表达式的语言
- 语句是执行一些动作的指令
- 表达式会计算产生一个值
**例子:**这个 y
代码块中的最后一行,不带分号表示的就是表达式,y
的值就是这个块中最后一个表达式的值
如果你把x + 3
加了一个分号变为这样,x + 3;
,会报错,y
的值是()
fn main() {
let x = 5;
let y = {
let x = 1;
x + 3
};
println!("{}", y);//输出 4
}
函数的返回值
- 在
->
符号后边声明函数返回值的类型,但是不可以为返回值命名 - 在 Rust ,返回值就是函数体里面,最后一个 表达式 的值
- 若想提前返回,需使用
return
关键字,并指定一个值- 大多数函数都是默认使用最后一个 表达式 作为返回值
例子:
fn main() {
println!("{}", f(3));
}
fn f(x: i32) -> i32 {
x + 5
}
返回多个值
rust的函数不支持返回多个值,但是我们可以利用元组来返回多个值,配合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)
}
可以看到,上例中,pow_2_3
函数接收一个i32
类型的值,返回其二次方和三次方的值,这两个值包装在一个元组中返回。在main
函数中,let
语句就可以使用模式匹配将函数返回的元组进行解构,将这两个返回值分别赋给p2
和p3
,从而可以得到789
二次方的值和三次方的值。
2)注释
跟正常的语言一样
/*
*注释
*/
//注释
4、控制流
1)if
else
-
if
表达式允许你根据条件来执行不同的代码分支- 条件必须是
bool
类型
- 条件必须是
-
if
表达式中,与条件相关联的代码块就叫做分支arm
-
可以在后边加上一个
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);
}
}
-
如何用
for
来实现 3 2 1的例子呢in (1 .. 4)
就是在[1,4),从 1 到 3 ,循环,.rev()
就是取反,从 1到 3 ,变为了,从 3 到 1
fn main() {
for number in (1..4).rev() {
println!("{}", number);
}
println!("结束")
}
- 把数组中的值,同步加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 上的数据必须拥有已知的固定的大小
- 编译时大小未知的数据或运行时大小可能发生变化的数据必须存放在 Heap 上
-
把值压到 Stack 上不叫 ”分配“(因为实际上不需要分配,数据在 Stack 上挨着放就可以了)
-
Heap 内存组织性差一些
- 当你把数据放入 Heap 时,你会请求一定数量的空间
- 操作系统在 Heap 里找到一块足够大的空间,把它标记为在用,并返回一个指针,也就是这个空间的地址
- 这个过程叫做在 Heap 上进行分配,有时仅仅称为 ”分配“
-
因为指针是已知固定大小的,可以把指针存放在 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);
}
内存与分配
-
字符串字面值
let a = "AA"
,在编译时就知道他的内容了,其文本内容直接被硬编码到最终的可执行文件中- 速度快、高效,是因为其不可变性
-
String 类型,为了支持可变性,需要在 Heap 上分配内存来保存编译时未知的文本内容
- 操作系统必须在运行时来请求内存
- 通过调用
String::from
实现
-
当用完 String 后,需要用某种方式把内存返还给操作系统
- 这步,在拥有GC的语言中,GC会跟踪并清理不再使用的内存
- 没有GC,就需要我们去识别内存何时不再使用,并调用代码将内存空间返还
- 如果忘了,那就是浪费内存
- 如果提前做的,变量就会非法
- 如果做了两次,也是Bug,必须一次分配对应一次释放
-
Rust 采用了不同的方式:对于某个值来说,当拥有它的变量走出范围时,内存会立即自动的交还给操作系统
-
drop
函数,变量走出作用域,Rust 会自动执行这个函数,释放空间
变量和数据交互的方式:
移动(Move)
基本数据类型:
- 多个变量可以使用同一个数据
- 整数是已知且固定大小的简单的值,这两个 5 被压到了 Stack 中
- 基本数据类型不存在什么 浅拷贝、深拷贝,只有引用类型涉及,因为基本数据类型操作都是在 Stack 上进行的
let x = 5;
let y = x;
String 类型(或者说用到了 Heap 的类型,也叫引用类型):
let s1 = String::from("hello");//第一步如图一所示
let s2 = s1; //第二步如图二所示
- 一个 String 由 3 部分构成(放在 Stack 中),存放字符串内容的部分在 Heap 上
- 一个指向存放字符串内容的内存的指针
- 一个长度(len:表示存放这个字符串所需的字节数)
- 一个容量 (capacity:指 String 从操作系统中总共获得内存的总字节数)
图一:
-
当把 s1 赋给 s2,String 的数据被复制了一份:
- 在 Stack 上复制了一份,指针、长度、容量
- 并没有复制指针所指向的 Heap 数据
图二:
- 当变量离开作用域时,Rust 会自动调用
drop
函数,并将变量使用的heap
内存释放 - 当
s1
,s2
离开作用域时,它们都会尝试释放相同的内存- 二次释放(double free)Bug
- 为了保证内存的安全
- Rust 没有尝试复制被分配的内存(Heap 中的存储内容没有被复制)
- Rust 让
s1
失效 - 当
s1
离开作用域的时候,Rust 不需要释放任何东西
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1);//会报错
}
-
这种操作,有点像浅拷贝(复制 Stack 中的索引信息,指向同一个 Heap 内存地址),但是Rust 后续删除了第一个变量,所以我们叫了一个新名字 移动(Move)
-
另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的 深拷贝(在 Stack 中创建新的索引,在 Heap 中创建新空间存储数据,这个新索引,指向新空间,数据是一样的)。因此,任何 自动 的复制可以被认为对运行时性能影响较小
( 因为都是Move,没对 Heap 进行操作)。
克隆(Clone)
- 如果真想对 Heap 上的 String 数据进行深度拷贝,而不仅仅是 Stack 上的索引数据,可以使用 Clone 方法(以后细说)
这样我们拿 s1
就不会报错了,因为他是这样的了,如图三
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}, {}", s1, s2);
}
图三:
Stack 上的数据进行复制
还记得我们上面说过,基本数据类型所有的操作都是在 Stack 上进行的么
-
Copy 这个 trait(特质) 在编译器的眼里代表的是什么意思呢?简单点总结就是说,如果一个类型 impl 了 Copy trait,意味着任何时候,我们可以通过简单的内存拷贝(C语言的按位拷贝memcpy)实现该类型的复制,而不会产生任何问题。有点像注解
-
Copy trait,用于像整数这样完全存放在 Stack 上面的类型
-
所有需要分配内存的都不是 Copy trait
-
一些拥有 Copy trait 的类型:
- 所有整数类型
- bool
- char
- 所有浮点类型
- 元组(Tuple),如果里面所有字段都是Copy的,那这个元组也是
- (i32,i32)这个是
- (i32,String)这个不是
5、所有权与函数(例子)
- 在语义上,将值传递给函数和把值赋给变量是类似的
- 将值传递给函数将发生移动和复制
例子:
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、返回值与作用域
函数在返回值的过程中同样也会发生所有权的转移
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
&
符号就表示引用:允许你引用某些值而不取得其所有权
s
就是个指针指向了s1
然后再指向Heap,这就是引用的原理
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的引用,也就是说,会指向一个已经被释放的内存空间,所以会直接报错
总结
- 关于引用,必须满足下列条件之一:
- 一个可以变的引用
- 任意数量的不可变引用
- 引用必须一直有效(不能指向空内存空间)
七、切片
1)一个例子引出的问题
-
Rust 另一种不持有所有权的数据类型:切片(slice)
-
一道题,编写一个函数:
-
它接受字符串作为参数
-
返回它在这个字符串里找到的第一个单词
-
如果函数没找到任何空格,就代表整个字符串就是一个单词,那么整个字符串返回
-
目前我们还没有学获取部分字符串的方法,所以我们先把空格所在位置的索引作为返回值,没找到空格就返回字符串的长度
fn main() {
let str = String::from("Hello World");
let r = first_word(&str);
println!("{}", r) //输出结果是5,空格所在位置的索引
}
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
算法就是拿到这个传入字符串的每个字节,看这个字节是否为空格
let bytes = s.as_bytes();
用这个方法,将传入字符串转换为字节数组
接下来,用for
循环,遍历字节数组,找到空格
我们需要for
拿到字节的元素进行与空格判断,如果相等,要返回索引
所以这个for
既要拿到字节数组的每个元素,又要拿到每个索引
是不是一下子就想到了元组
for (i, &item) in bytes.iter().enumerate()
这里面enumerate()
返回了一个元组,i
是索引,而元组中的 &item
是单个字节
注意:
如果这样使用
fn main() {
let mut str = String::from("Hello World");
let r = first_word(&str);
str.clear();//加了一步:清除原本的字符串
//r不是你拿到的索引么,你现在想要根据索引去截取字符串,但是原本的字符串已经被清除了
//字符串和你获取的索引值没有任何关联关系
}
fn first_word(s: &String) -> usize {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
s.len()
}
如何解决这种情况呢,Rust 为这个问题提供了一个解决方法:字符串 slice(切片)。
2)字符串切片
-
字符串切片是指,向字符串中一部分内容的引用
-
一个例子:
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 )
左闭右开,内部数字为引用字符串索引
s
是字符串索引,world
是切片,切片从6开始
3)切片的语法糖
语法糖:不改变结果的情况下,让代码更简洁
- 如果切片的开始是第一个索引,可以忽略不写
- 如果切片的结束是最后一个索引,也就是字符串的长度的话,也可以忽略不写
fn main() {
let s = String::from("Hello World");
let hello = &s[..5];//原本写法 &s[0..5]
let world = &s[6..];//原本写法 &s[6..11]
println!("{}", hello);
println!("{}", world);
}
- 那么如果你理解了这种思想,我想另一种语法糖你也能猜到
let all = &s[..] //引用整个字符串,直接忽略开始结束索引
4)修改例子代码
- 我们现在知道了如何截取字符串,那么让我们改一下最开始的例子中的代码
fn main() {
let str = String::from("Hello World");
let r = first_word(&str);
println!("{}", r) //输出结果是5,空格所在位置的索引
}
fn first_word(s: &String) -> &str {//“字符串 slice” 的类型声明写作 &str
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];//返回第一个空格前的字符串
}
}
&s[..]//没有空格返回整个字符串
}
-
还记得,当我们获取第一个单词结尾的索引后,接着就清除了字符串导致索引就无效的 bug 吗?
(虽然编译不错,但是逻辑上是错误的)
-
我们再次尝试清除字符串,会报错
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();
用的是可变引用
还记得之前学的引用的限制么:不可以同时拥有一个可变引用和一个不变的引用
所以会直接在编译时报错
5)字符串字面值&str和String
Rust将字符串分为两种:
- &str :固定长度字符串(也称字符串字面量)
- String :可以随意改变其长度。
两者区别
- &str字符串类型存储于栈上,str字符串序列存储于程序的静态只读数据段或者堆内存中。由两部分组成:
- 指向字符串序列的指针
- 记录长度的值
- String类型本质是一个成员变量为Vec类型的结构体,所以它是直接将字符内容存放于堆中的。由三部分组成
- 执行堆中字节序列的指针(as_ptr方法)
- 记录堆中字节序列的字节长度(len方法)
- 堆分配的容量(capacity方法)
几种声明方式
fn main(){
//1.第一种方式:通过String的new创建一个空的字符串
let mut my_str = String::new();//不能有字符变量
my_str.push_str("my_str"); //为这个空的字符串变量使用push_str方法添加一个值
//2.第二种方式 通过String的from创建一个字符串
let mut my_str2 = String::from("my_str");
my_str2.push_str("2");
//3.第三种方式,直接书写字符串字面量
let mut my_str3 = "my_str3"; // &str
//4.第四种方式 通过to_string把字面量变成一个字符串
let mut my_str4 = "my_str".to_string();
my_str4.push_str("4");
//5.第五种方式 通过with_capacity创建一个带有预先分配的初始缓冲区的空字符串
//如果已知String的最终大小,String::with_capacity则应首选。
let mut my_str5 = String::with_capacity(7);
my_str5.push_str("my_str5");
println!("{} , {} , {} , {} , {}" , my_str , my_str2 , my_str3 , my_str4 , my_str5);
}
6)将字符串切片作为参数传递
fn first_word(s: &String) -> &str {
原本传入String类型- 有经验的 Rust 开发者会采用
&str
作为参数类型,因为这样就可以同时接收String
和&str
类型了 - 变为这样
fn first_word(s: &str) -> &str {
- 使用字符串切片,直接调用该函数
- 使用
String
,可以创建一个完整的String
切片来调用该函数
- 让我们的API更加通用,而且不会损失功能
fn main() {
let str1 = String::from("Hello World");
let x = first_word(&str1);
let str2 = "hello world";
let y = first_word(str2);
println!("{}, {}", x, y) //输出结果 Hello, hello
}
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[..]
}
7)其它类型的切片
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];//对数组切片,从索引为1到索引为3
}
八、Struct
1)定义并实例化struct
什么是struct
- struct,结构体
- 自定义的数据类型
- 让我们能为相关联的值命名,打包组合成有意义的组合
定义struct
-
使用
struct
关键字,并为整个 struct 命名 -
在花括号内,为所有**字段(Field)**定义名称和类型
-
例子:
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
实例化struct
- 想要使用struct ,需要创建 struct 的实例
- 为每个字段指定具体值
- 无需按照声明的顺序进行指定
- 例子:
let user1 = User{
email:String::from("1841632321@qq.com"),
username:String::from("李泽辉"),
sign_in_count:1,
active:true,
}
取得 struct 里面的某个值
- 使用点标记法
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: String::from("1841632321@qq.com"),
username: String::from("李泽辉"),
sign_in_count: 1,
active: true,
};
println!("{}", user1.username);
println!("{}", user1.email);
println!("{}", user1.sign_in_count);
println!("{}", user1.active);
}
修改 struct 里面的某个值
- 修改了
username
,注意要给实例user1
加mut
因为是可变的 - 而一旦这个实例
user1
是可变的,那么示例中的所有字段都是可变的
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let mut user1 = User {
email: String::from("1841632321@qq.com"),
username: String::from("李泽辉"),
sign_in_count: 1,
active: true,
};
user1.username = String::from("李大聪明");
println!("{}", user1.username);
println!("{}", user1.email);
println!("{}", user1.sign_in_count);
println!("{}", user1.active);
}
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 更新语法:
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
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
-
可定义类似 Tuple 的 Struct ,叫做 Tuple Struct
- 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)
结构体数据的所有权
我们声明User
,里面的username
和 email
时候用的是String
而不是&str
sign_in_count
和 active
又是标量类型
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
这个结构体拥有其所有数据的所有权,因为结构体失效的时候会释放所有字段。
这就是为什么本章的案例中使用了 String
类型而不使用 &str
的原因。
但这不意味着结构体中不定义引用型字段,这需要通过"生命周期"机制来实现。
生命周期确保结构体引用的数据有效性跟结构体本身保持一致。如果你尝试在结构体中存储一个引用而不指定生命周期将是无效的
struct User {
username: &str,//这样会报错
email: &str,//报错,没有生命周期
sign_in_count: u64,
active: bool,
}
现在还难以说明"生命周期"概念,所以只能在后面章节说明。
2)struct 的例子
例子需求
计算长方形面积
实现1:
fn main() {
let w = 30;
let h = 50;
println!("面积是{}", area(w, h));
}
fn area(width: u32, height: u32) -> u32 {
width * height
}
上面的代码可以运行,并且计算出长方形的面积,但我想让它更好一些,我们来通过元组重构一下
使用元组重构
实现2:
fn main() {
let rec: (u32, u32) = (30, 50);
println!("面积是{}", area(rec));
}
fn area(dimensions: (u32, u32)) -> u32 {
dimensions.0 * dimensions.1
}
在某种程度上说,这个程序更好一点了。元组帮助我们增加了一些结构性,并且现在只需传一个参数。
不过在另一方面,这个版本却有一点不明确了:元组并没有给出元素的名称,所以计算变得更费解了,因为不得不使用索引来获取元组的每一部分
必须牢记 width
的元组索引是 0
,height
的元组索引是 1
。
如果其他人要使用这些代码,他们必须要搞清楚这一点,并也要牢记于心。很容易忘记或者混淆这些值而造成错误,因为我们没有在代码中传达数据的意图。
我们用结构体重构一下
使用结构体重构
实现3:
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
}
这里我们定义了一个结构体并称其为 Rectangle
。在大括号中定义了字段 width
和 height
,类型都是 u32
。接着在 main
中,我们创建了一个具体的 Rectangle
实例,它的宽是 30,高是 50。
函数 area
现在被定义为接收一个名叫 rectangle
的参数,其类型是一个结构体 Rectangle
实例的不可变借用。我们希望借用结构体而不是获取它的所有权,这样 main
函数就可以保持 rect1
的所有权并继续使用它,所以这就是为什么在函数签名和调用的地方会有 &
。
现在!area
的函数签名现在明确的阐述了我们的意图:使用 Rectangle
的 width
和 height
字段,计算 Rectangle
的面积。这表明宽高是相互联系的,并为这些值提供了描述性的名称而不是使用元组的索引值 0
和 1
。结构体胜在更清晰明了。
3)输出结构体
想要输出Rectangle
这个结构体
我们通过println!
,但是会报错
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rec = Rectangle {
width: 30,
height: 50,
};
println!("面积是{}", rec);//报错
}
当我们运行这个代码时,会出现带有如下核心信息的错误:
error[E0277]: Rectangle doesn't implement std::fmt::Display
println!
宏能处理很多类型的格式,不过,{}
默认告诉 println!
使用被称为 Display
的格式:意在提供给直接终端用户查看的输出。
不过对于结构体,println!
应该用来输出的格式是不明确的,因为这有更多显示的可能性:是否需要逗号?需要打印出大括号吗?所有字段都应该显示吗?由于这种不确定性,Rust 不会尝试猜测我们的意图,所以结构体并没有提供一个 Display
实现**。**
看一下下面的帮助:
= note: in format strings you may be able to use {:?} (or {:#?} for pretty-print) instead
我们的输出照着试一下
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rec = Rectangle {
width: 30,
height: 50,
};
println!("面积是{:?}", rec);//这里试一下
}
我们看到,虽然还是报错,但是报错变了,他说我们的结构体没有实现 “Debug”
我们必须为结构体显式选择这个功能。为此,在结构体定义之前加上 #[derive(Debug)]
注解
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rec = Rectangle {
width: 30,
height: 50,
};
println!("面积是{:?}", rec);
}
现在我们成功输出了
如果想让他好看一些,可以使用 {:#?}
替换 println!
字符串中的 {:?}
#[derive(Debug)]
struct Rectangle {
width: u32,
height: u32,
}
fn main() {
let rec = Rectangle {
width: 30,
height: 50,
};
println!("面积是{:#?}", rec);
}
4)struct 上面的方法
-
方法和函数很类似:
fn
关键字、名称、参数、返回值 -
方法与函数不同之处:
- 方法是在 struct( 或enum、trait对象)的上下文中定义
- 方法的第一个参数是
self
,表示方法被调用的 struct 实例
定义方法
- 在
impl
块里定义方法 - 方法的第一个参数可以
&self
借用,也可以获得其所有权,或者可变借用(和其它参数一样)
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代表rec2
println!("rec1能否包括rec3:{}", rec1.can_hold(&rec3));
}
返回:
rec1能否包括rec2:true
rec1能否包括rec3:false
关联函数
- 可以在
impl
块里定义不把self
作为第一个参数的函数,他们叫关联函数(是函数,不是方法,不是通过实例对象.
进行调用的)- 例如:
String::from()
就是一个关联函数
- 例如:
- 关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以提供一个关联函数,它接受一个维度参数并且同时作为宽和高,这样可以更轻松的创建一个正方形
Rectangle
而不必指定两次同样的值:
#[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);//输出一下
}
分成多个块
impl Rectangle {
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
impl Rectangle {
fn can_hold(&self, other: &Rectangle) -> bool {
self.width > other.width && self.height > other.height
}
}
这样也是可以的,分成两部分
九、枚举与模式匹配
1)枚举
- 枚举允许我们列举所有可能的值来定义一个类型
定义枚举
- IP地址:IPv4,IPv6
enum ipAddrKind {
V4,//枚举中所有可能的值叫做变体
V6,
}
创建枚举值
-
let four = ipAddrKind::V4;
-
let six = ipAddrKind::V6;
-
创建枚举值,并且传入方法中
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);
}
为枚举定义方法
impl
关键字- 我们尝试把存入的值显示出来
#[derive(Debug)]
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
impl Message {
fn call(self) -> Message {
self
}
}
fn main() {
let q = Message::Quit;
let m = Message::Move { x: 10, y: 22 };
let w = Message::Write(String::from("字符串"));
let c = Message::ChangeColor(0, 255, 255);
let x = m.call();
println!("{:#?}", x);
}
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); //定义一个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
必须都有分支
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
-
代码组织:
目前为止我们都是在一个文件中编写的,主要是为了方便学习 Rust 语言的语法和概念。
对于一个工程来讲,组织代码是十分重要的。因为你想在脑海中通晓整个程序,那几乎是不可能完成的。通过对相关功能进行分组和划分不同功能的代码,你可以清楚在哪里可以找到实现了特定功能的代码,以及在哪里可以改变一个功能的工作方式。 -
模块系统:
- 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();//相对路径
}
会报错module hosting is private
为什么crate
和front_of_house
不报错而是从hosting
开始呢?
因为fn main
和crate
, front_of_house
一样都是根节点,根节点之间访问无论私有公有
能放入 mod 内部中的一切都是默认是私有的,要把改为共有 pub
- 额外知识点
- 父级模块无法访问子模块中的条目
- 子模块可以使用所有祖先模块中的条目
- 公有是 pub
mod front_of_house {
pub mod hosting {
pub 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
- pub 放在 struct 前:
- struct 是公共的
- struct 中的字段默认是私有的
mod back_of_house {
pub struct Breakfast {
pub x: String,//公有
y: String,//私有
}
}
pub enum
- pub 放在 enum 前面
- enum 是公共的
- 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来包含它
8)引入外部包出错的解决方法
VsCode导入包后一直转圈
首先要停掉Rust server
Ctrl + Shift + P 快捷键,输入Stop那个,停掉服务
改用命令行,像这样
可以看到它说 Blocking waiting for file lock on package cache lock
把Cargo
路径下的文件.package-cache
删除
一般是在这个路径下的
删除掉之后,接着在你的项目路径,执行cargo build
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
使用Vector存储多个值
Vec<T>
,叫做vector- 由标准库提供
- 可存储多个值
- 只能存储相同类型的数据
- 值在内存中连续存放
创建Vector
Vec::new
函数
fn main() {
let v: Vec<i32> = Vec::new();
}
因为 Vec::new()
是创建一个空的vector,里面没有元素,Rust无法进行推断
所以要显示的声明内部是什么数据类型
- 使用初始值创建
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::String
let 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, 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!:
连接多个字符串
如果用 +
连接多个显得很笨重
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
- 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<T, E>
- 不可恢复错误:panic! 宏
不可处理错误与 panic!
- 当 panic!宏执行(默认情况下):
- 你的程序会打印一个错误信息
- 展开(unwind)、清理调用栈(Stack)
- 退出程序
为应对 panic,展开或中止(abort)调用栈
-
默认情况下,当 panic 发生:
- 程序展开调用栈(工作量大)
- Rust 沿着调用栈往回走
- 清理每个遇到的函数中的数据
- 或立即中止调用栈:
- 不进行清理,直接停止程序
- 内存需要OS(操作系统)进行清理
- 程序展开调用栈(工作量大)
-
如果你需要项目的最终二进制文件越小越好
- panic 时通过在 Cargo.toml 的
[profile]
部分增加panic = 'abort'
,可以由展开切换为终止。 - 例如,如果你想要在release模式(生产环境下)中 panic 时直接终止:
- panic 时通过在 Cargo.toml 的
一个小例子
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 枚举
-
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
where
T: 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>
这样
ce = 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
,在第七章 重新温故一下,
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
where
T: 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>
这样