弱小和无知不是生存的障碍,傲慢才是
参考:
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
边看B站视频边看笔记效果更佳
https://www.bilibili.com/video/BV1hp4y1k7SV?share_source=copy_web
1.2
节介绍了安装Rust
Windows + R cmd
rustc --version
Windows + R cmd
cargo --version
cargo new hello_cargo[项目名]
cargo build
cargo run
先编译后运行,如果编译过了,直接运行可执行二进制文件
修改源文件会重新编译
该二进制文件会在这个目录下
hello_cargo[项目名]\target\debug\hello_cargo.exe
cargo check
cargo check,检查代码确保能通过编译,但是不产生任何可执行文件
cargo check 要比 cargo build 快很多
编写代码可以连续反复使用cargo check 检查代码,提高效率
cargo build --release
两种构建方式一个为了开发,一个为了发布
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
Result这个枚举类型中有两个"变体"(枚举类型中的固定值称作变体)
Ok 和 Err
返回的是 Ok,表示操作成功了,附加成功的字符串
如果返回的是 Err,表示操作失败了,附加失败的字符串以及报错信息
然后调用.expect()
方法
如果read_line()
返回的io::Result
实例的值是Ok,那么expect()
会提取Ok的附加值,作为结果返回给用户
如果read_line()
返回的io::Result
实例的值是Err,那么expect()
会把程序中断,并把传入的字符串信息显示出来
println!("你猜测的数是{}",guess);
{ } 占位符
use std::io;
fn main() {
println!("猜数");
let mut guess = String::new();
io::stdin().read_line(&mut guess).expect("无法读取行");
println!("你猜测的数是{}",guess);
}
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(左闭右开 )
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);
}
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
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!("猜对了"),
}
}
把下面的代码注释掉,默认是i32
当把 guess 字符串解析为u32
时,secret_number也变为了u32
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 的情况下,告诉用户输入数字,并且结束本次循环(然后继续从头走循环)
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;
}
}
}
}
let
关键字mut
,就可使变量可变 Ctrl+左键 查看示例常量(constant),常量在绑定值以后也是不可变的,但它与不可变的变量有很多区别
不可以使用 mut ,常量永远都是不可变的
声明常量使用 const 关键字,它的类型必须被标注
常量可以在任何作用域内进行声明,包括全局作用域
在程序运行期间,常量在其声明的作用域内一直有效,因此可以作为不同代码之间共享值
const MAX_POINTS: u32 = 10_0000;
fn main() {
const MAX_POINTS: u32 = 10_0000;
}
常量只可以绑定到常量表达式,无法绑定到函数的调用结果或只能在运行时才能计算的值(就是在编译期就确定了)
命名规范:Rust 里常量使用全大写字母,每个单词之间用下划线分开,例如:
const MAX_POINTS: u32 = 10_0000
数字增加下划线只是为了增加可读性
可以使用相同的名字声明新的变量,新的变量就会 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);
}
Rust 是静态编译语言,在编译时必须知道所有变量的类型
基于使用的值,编译器通常能够推断出它的具体类型 Ctrl+左键 查看示例
但如果可能的值比较多(例如把 String
转成整数的 parse()
方法),就必须添加类型的标注,否则编译会报错
let str: u32 = "2".parse().expect("Not a number");
一个标量类型代表一个单个的值
Rust 有四个主要的标量类型:
u32
就是一个无符号的整数类型,占据 32 位的空间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;
true
、false
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 = '';
let tup: (i32,f64,char) = (100,5.1,'a');//创建Tuple
println!("{},{},{}",tup.0,tup.1,tup.2);
let tup: (i32,f64,char) = (100,5.1,'a');
let (x, y, z) = tup;//给变量赋值
println!("{},{},{}", x, y, z);
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 灵活
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]]);
但是运行会报错
声明函数使用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);
}
**例子:**这个 y
代码块中的最后一行,不带分号表示的就是表达式,y
的值就是这个块中最后一个表达式的值
如果你把x + 3
加了一个分号变为这样,x + 3;
,会报错,y
的值是()
fn main() {
let x = 5;
let y = {
let x = 1;
x + 3
};
println!("{}", y);//输出 4
}
->
符号后边声明函数返回值的类型,但是不可以为返回值命名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
二次方的值和三次方的值。
跟正常的语言一样
/*
*注释
*/
//注释
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
中可能成为结果的返回值类型必须是一样的,为了安全,编译时就要确定类型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
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!("结束")
}
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 就保证内存安全
Rust 的核心特性就是所有权
所有程序在运行时都必须管理它们使用计算机内存的方式
Rust 采用了第三种方式
Stack 会按值的接收顺序来存储,按相反的顺序将它们移除(先进后出,后进先出)
所有存储在 Stack 上的数据必须拥有已知的固定的大小
把值压到 Stack 上不叫 ”分配“(因为实际上不需要分配,数据在 Stack 上挨着放就可以了)
Heap 内存组织性差一些
因为指针是已知固定大小的,可以把指针存放在 Stack 上(也就是说,Stack 存储着 Heap 的指针)
把数据压到 Stack 上要比在 Heap 上分配快得多
因为(入栈时)操作系统无需为存储新数据去搜索内存空间;其位置总是在栈顶。
相比之下,在堆上分配内存则需要更多的工作,这是因为操作系统必须首先找到一块足够存放数据的内存空间,并接着做一些记录为下一次分配做准备。
fn main() {
//s 不可用
let s = "hello";//s 可用
//可以对 s 进行相关操作
}//s 作用域到此结束,s 不可用
String 类型比那些基础标量数据类型更复杂
基础数据类型存放在 Stack 上,离开作用域就会弹出栈
我们现在用一个存储在 Heap 上面的类型,来研究 Rust 是如何回收这些数据的
String 会在 Heap 上分配,能够存储在编译时未知数量的文本
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 后,需要用某种方式把内存返还给操作系统
Rust 采用了不同的方式:对于某个值来说,当拥有它的变量走出范围时,内存会立即自动的交还给操作系统
drop
函数,变量走出作用域,Rust 会自动执行这个函数,释放空间
基本数据类型:
let x = 5;
let y = x;
String 类型(或者说用到了 Heap 的类型,也叫引用类型):
let s1 = String::from("hello");//第一步如图一所示
let s2 = s1; //第二步如图二所示
图一:
当把 s1 赋给 s2,String 的数据被复制了一份:
图二:
drop
函数,并将变量使用的 heap
内存释放s1
,s2
离开作用域时,它们都会尝试释放相同的内存
s1
失效s1
离开作用域的时候,Rust 不需要释放任何东西 fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1);//会报错
}
这种操作,有点像浅拷贝(复制 Stack 中的索引信息,指向同一个 Heap 内存地址),但是Rust 后续删除了第一个变量,所以我们叫了一个新名字 移动(Move)
另外,这里还隐含了一个设计选择:Rust 永远也不会自动创建数据的 深拷贝(在 Stack 中创建新的索引,在 Heap 中创建新空间存储数据,这个新索引,指向新空间,数据是一样的)。因此,任何 自动 的复制可以被认为对运行时性能影响较小
( 因为都是Move,没对 Heap 进行操作)。
这样我们拿 s1
就不会报错了,因为他是这样的了,如图三
fn main() {
let s1 = String::from("hello");
let s2 = s1.clone();
println!("{}, {}", s1, s2);
}
图三:
还记得我们上面说过,基本数据类型所有的操作都是在 Stack 上进行的么
Copy 这个 trait(特质) 在编译器的眼里代表的是什么意思呢?简单点总结就是说,如果一个类型 impl 了 Copy trait,意味着任何时候,我们可以通过简单的内存拷贝(C语言的按位拷贝memcpy)实现该类型的复制,而不会产生任何问题。有点像注解
Copy trait,用于像整数这样完全存放在 Stack 上面的类型
所有需要分配内存的都不是 Copy trait
一些拥有 Copy trait 的类型:
例子:
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
函数在返回值的过程中同样也会发生所有权的转移
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
}
drop
函数清理,除非数据所有权移动到另一个变量上了 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返回一个副本就可以了
}
这种做法,不得不把变量作为参数传入,然后又作为返回值传出,很繁琐
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,这就是引用的原理
当一个函数使用引用,而不是一个真实的值作为它的参数,我们就管这个行为叫做借用
那我们是否可以修改借用的东西呢?
不可以
和变量一样,引用默认也是不可变的
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 pointer)
悬垂指针:一个指针引用了内存中的某个地址,而这块内存可能已经释放分配给其他人使用了
在 Rust 中,编译器可保证引用永远不会处于悬垂状态
我们尝试创建一个悬垂引用,Rust 会通过一个编译时错误来避免:
fn main() {
let r = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}//离开这个方法作用域,s销毁了,而这个方法,返回了s的引用,也就是说,会指向一个已经被释放的内存空间,所以会直接报错
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(切片)。
字符串切片是指,向字符串中一部分内容的引用
一个例子:
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开始
语法糖:不改变结果的情况下,让代码更简洁
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[..] //引用整个字符串,直接忽略开始结束索引
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();
用的是可变引用
还记得之前学的引用的限制么:不可以同时拥有一个可变引用和一个不变的引用
所以会直接在编译时报错
Rust将字符串分为两种:
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);
}
fn first_word(s: &String) -> &str {
原本传入String类型&str
作为参数类型,因为这样就可以同时接收 String
和 &str
类型了fn first_word(s: &str) -> &str {
String
,可以创建一个完整的String
切片来调用该函数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[..]
}
fn main() {
let a = [1, 2, 3, 4, 5];
let slice = &a[1..3];//对数组切片,从索引为1到索引为3
}
使用 struct
关键字,并为整个 struct 命名
在花括号内,为所有**字段(Field)**定义名称和类型
例子:
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
let user1 = User{
email:String::from("[email protected]"),
username:String::from("李泽辉"),
sign_in_count:1,
active:true,
}
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: String::from("[email protected]"),
username: String::from("李泽辉"),
sign_in_count: 1,
active: true,
};
println!("{}", user1.username);
println!("{}", user1.email);
println!("{}", user1.sign_in_count);
println!("{}", user1.active);
}
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("[email protected]"),
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);
}
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 User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
fn main() {
let user1 = User {
email: String::from("[email protected]"),
username: String::from("李泽辉"),
sign_in_count: 1,
active: true,
};
let user2 = User {
email: String::from("新邮箱"),//改变了email
..user1//其他的不改变可以直接这样写,表示这个新实例中剩下的没被赋值的字段(除了email)和user1的一样
};
}
可定义类似 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,
}
现在还难以说明"生命周期"概念,所以只能在后面章节说明。
计算长方形面积
实现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
。结构体胜在更清晰明了。
想要输出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);
}
方法和函数很类似:fn
关键字、名称、参数、返回值
方法与函数不同之处:
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
}
}
这样也是可以的,分成两部分
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);
}
Option
enum Option<T> {
Some(T),
None,
}
可以直接使用,不需要像正常的枚举一样,Option::Some(5);
let some_number = Some(5); //std::option::Option
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
有一个结构体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
#[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
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),
}
}
Option有两个变体,一个None一个Some
必须都有分支
fn main() {}
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1),
}
}
_
不用穷举所有可能性了
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
代码组织:
目前为止我们都是在一个文件中编写的,主要是为了方便学习 Rust 语言的语法和概念。
对于一个工程来讲,组织代码是十分重要的。因为你想在脑海中通晓整个程序,那几乎是不可能完成的。通过对相关功能进行分组和划分不同功能的代码,你可以清楚在哪里可以找到实现了特定功能的代码,以及在哪里可以改变一个功能的工作方式。
模块系统:
Crate 的类型有两种:
Crate Root(Crate 的根):
mod
声明,那么模块文件的内容将在编译之前被插入 crate 文件的相应声明处一个Package:
一个例子:
我们创建一个新的项目(一个项目就是一个包)
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会默认把这个文件作为根
建立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
为了在Rust的模块中找到某个条目,需要使用路径
路径的两周形式
路径至少由一个标识符组成,标识符之间使用 ::
如果定义的部分和使用的部分总是一起移动,用相对路径,可以独立拆解出来,用绝对路径
例子:
mod front_of_house {
mod hosting {
fn add_to_waitlist() {
println!("1111");
}
}
}
fn main() {
crate::front_of_house::hosting::add_to_waitlist();//绝对路径
front_of_house::hosting::add_to_waitlist();//相对路径
}
会报错module hosting is private
为什么crate
和front_of_house
不报错而是从hosting
开始呢?
因为fn main
和crate
, front_of_house
一样都是根节点,根节点之间访问无论私有公有
能放入 mod 内部中的一切都是默认是私有的,要把改为共有 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();
}
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
方法
mod back_of_house {
pub struct Breakfast {
pub x: String,//公有
y: String,//私有
}
}
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
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() {}
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();
}
[dependencies]
rand = "0.5.5"
VsCode导入包后一直转圈
首先要停掉Rust server
Ctrl + Shift + P 快捷键,输入Stop那个,停掉服务
改用命令行,像这样
可以看到它说 Blocking waiting for file lock on package cache lock
把Cargo
路径下的文件.package-cache
删除
一般是在这个路径下的
删除掉之后,接着在你的项目路径,执行cargo build
如果使用同一个包或模块下的多个条目
可以使用嵌套路径,在同一行内将上述条目进行引入
use std::cmp::Ordering;
use std::io;
变为
use std::{cmp::Ordering, io};
特殊情况:
use std::io;
use std::io::Write;
变为
use std::io::{self, Write};
我么可以使用 * 把路径中所有的公共条目都引入到作用域
把这个路径下的所有都引入了
use std::collections::*;
谨慎使用
初始内容( 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();
}
Vec
,叫做vector
Vec::new
函数fn main() {
let v: Vec<i32> = Vec::new();
}
因为 Vec::new()
是创建一个空的vector,里面没有元素,Rust无法进行推断
所以要显示的声明内部是什么数据类型
Vet
,使用 vec! 宏fn main() {
let v = vec![1,2,3];
}
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可以推断类型了
fn main() {
let v = vec![1,2,3];
}//到这里就自动被清理了
但是如果涉及到对 vector 里面的元素有引用的话,就会变复杂
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是可变的借用,而前面已经用了不可变的借用,违反了规则
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
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 中类型的原因在于它需要知道储存每个元素到底需要多少内存。
Rust 的核心语言层面,只有一个字符串类型:字符串切片 str(或&str)
字符串切片:对存储在其他地方、UTF-8编码的字符串的引用
String 类型:
通常说的字符串就是指的 String 和 &str
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
}
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)的技术
连接多个字符串
如果用 +
连接多个显得很笨重
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
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
的封装。
“Hola” 的 Vec
的长度是四个字节:这里每一个字母的 UTF-8 编码都占用一个字节。
"Здравствуйте"是中如果你要返回З
你需要返回两个字节,那么你返回哪一个呢?
为了避免返回意外的值并造成不能立刻发现的 bug,Rust 根本不会编译这些代码,并在开发过程中及早杜绝了误会的发生。
还有一个原因是,索引操作预期总是需要常数时间 (O(1))。但是对于 String
不可能保证这样的性能,因为 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/这里找
请看前面的第七章:切片
键值对的形式存储数据,一个 Key 对应一个 Value
Hash 函数:决定如何在内存中存放 K 和 V
适用场景:通过 K(任何类型)来寻找数据,而不是通过索引
HashMap 是同构的,所有的 K 是同一类型,所有的 V 是同一类型
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内部类型
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")
.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
所包含的类型。
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);
}
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);
}
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
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
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}
entry
方法:检查指定的 K 是否对应一个 V
or_insert()
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 就不会进行插入
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 发生:
如果你需要项目的最终二进制文件越小越好
[profile]
部分增加 panic = 'abort'
,可以由展开切换为终止。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
enum Result<T, E> {
Ok(T),
Err(E),
}
T:操作成功情况下,Ok 变体里返回的数据的类型
E:操作失败情况下,Err 变体里返回的错误的类型
这个 f
就是 Result
类型,成功返回File
,失败返回Error
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);
}
});
}
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
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
之前所讲的是接收到错误的处理方式,但是如果我们自己编写一个函数在遇到错误时想传递出去怎么办呢?
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
错误返回io::String
的返回值类型
第15-18
行: f
代表打开的文件,从中读取字符串,赋给 s
,成功返回读取的字符串给 Result
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函数()
Result
use std::error::Error;
use std::fs::File;
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
代码说明:
第4行:main 函数不发生错误返回()
,发生错误返回Box
,Box
是 trait
对象,可以简单的理解为任意类型的错误
panic!
在定义一个可能失败的函数时,优先考虑返回 Result
否则就panic!
()
panic!
的场景panic!
程序会中止,代码在测试环境可以用,生产环境尽量不要有panic!
,可以用panic!
作为一个明显的标记)panic!
就代表测试没通过)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
}
fn largest(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
,又会有其他的报错,我们后面会解决这个问题
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 };
}
类似于结构体,枚举也可以在其成员中存放泛型数据类型。第六章我们使用过了标准库提供的 Option
枚举,让我们再看看:
enum Option<T> {
Some(T),
None,
}
现在这个定义看起来就更容易理解了。如你所见 Option
是一个拥有泛型 T
的枚举,它有两个成员:Some
,它存放了一个类型 T
的值,和不存在任何值的None
。通过 Option
枚举可以表达有一个可能的值的抽象概念,同时因为 Option
是泛型的,无论这个可能的值是什么类型都可以使用这个抽象。
枚举也可以拥有多个泛型类型
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
表示实现的方法用到了泛型,Point
表示传的参数是Point
类型的携带的数据是T
泛型
第7
行:方法ret
,传了&self
他自己就是Point
,其实是传的p
,返回了一个泛型T
第15
行:输出方法,p
的ret
方法,传入p
给&self
,返回p
的x
也就是&self.x
在代码第6
行,不需要写impl
要直接写成这样impl Point
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
是 i32
, U
是f64
第17
行:p1
是Point
,T
是 字符串切片&str
, U
是char
第19
行:p1.mixup(p2)
,用到的这个mixup()
方法,直接去看代码第7
行
第7
行:
fn mixup
声明了会用
泛型
(self, other: Point
传递的第一个参数是self
,其实就是p1
,第二个参数是other: Point
,传递的是p2
,特意说明了Point
不能写Point
,因为T
U
已经分别代表i32
和f64
了,就要用两个新的泛型来代表&str
和char
也就是V
和W
-> Point
返回值是一个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
类型
下面的代码在编译时会怎么样呢?
let integer = Some(5);
let float = Some(5.0);
当 Rust 编译这些代码的时候,它会进行单态化。编译器会读取传递给 Option
的值并发现有两种 Option
:一个对应 i32
另一个对应 f64
。为此,它会将泛型定义 Option
展开为 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);
}
这意味着在使用泛型时没有运行时开销,在编译期就已经产生具体定义的方法了
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
在本地的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
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 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
,·这里面
表示有两个不同类型的参数,一个实现了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 {};
}
}
>
比较的问题关于我们在第十三章,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
行:
实现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 的泛型参数的 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
是ToString
的实现,传入的参数T
只有满足实现fmt::Display
才可以使用to_string
方法
main.js
,3
实现了fmt::Display
所以拥有to_string
方法
fn main() {
let s = 3.to_string();
}
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
的操作都是无法进行的
说明:x
这个变量的生命周期被标记为'a
,y
这个变量的生命周期被标记为'b
,在编译时,Rust发现,x
拥有生命周期'a
,但是它引用了另一个拥有生命周期 'b
的对象,由于生命周期 'b
比生命周期 'a
要小,被引用的对象比它的引用者存在的时间短,程序被Rust拒绝编译。
我们来看一下没有产生悬垂引用且可以正确编译的例子
说明:x
的生命周期'a
,引用了y
的生命周期'b
,被引用的生命周期,长于引用的生命周期
这个程序很简单,传入两个字符串切片,哪个长,返回哪个
代码第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)生命周期标注
中会谈到
'
为开头'a
&
符号后面&i32
一个引用&'a i32
带有显式生命周期的引用&'a mut i32
带有显式生命周期的可变引用<>
里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
}
}
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
}
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
使用时候,没有结束,不报错
我们知道这个件事:每个引用都有生命周期,而且需要为使用生命周期的函数或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 的历史是因为更多的明确的模式被合并和添加到编译器中是完全可能的,未来需要手动标注的生命周期会越来越少。
编译器使用 3
个规则在没有显式标注生命周期的情况下,来确定引用的生命周期
1
:应用于输入生命周期2、3
:应用于输出生命周期3
个规则后,仍然有无法确定生命周期的引用——>报错fn
定义和 impl
块规则 1
:每个引用类型的参数都有自己的生命周期
规则 2
:如果只有 1 个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数
规则 3
:如果有多个输入生命周期参数,但其中一个是&self
或&mut self
(是一个对象的方法:在结构体、枚举类型、trait对象中的函数被称为方法)
那么 self
的生命周期会被赋给所有输出生命周期参数
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 {
现在这个函数签名中的所有引用都有了生命周期,如此编译器可以继续它的分析而无须程序员显式的标记这个函数签名中的生命周期。
再次假设自己是编译器
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 {
'static
是一个特殊的生命周期,表示:整个程序的执行期
'static
生命周期,我们也可以选择像下面这样标注出来:'static
的。fn main() {
let s: &'static str = "I have a static lifetime.";
}
你可能在错误信息的帮助文本中见过使用 'static
生命周期的建议,不过将引用指定为 'static
之前,思考一下这个引用是否真的在整个程序的生命周期里都有效。你可能会考虑希望它一直有效,如果可能的话。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个 'static
的生命周期。
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
使用时候,没有结束,不报错
我们知道这个件事:每个引用都有生命周期,而且需要为使用生命周期的函数或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 的历史是因为更多的明确的模式被合并和添加到编译器中是完全可能的,未来需要手动标注的生命周期会越来越少。
编译器使用 3
个规则在没有显式标注生命周期的情况下,来确定引用的生命周期
1
:应用于输入生命周期2、3
:应用于输出生命周期3
个规则后,仍然有无法确定生命周期的引用——>报错fn
定义和 impl
块规则 1
:每个引用类型的参数都有自己的生命周期
规则 2
:如果只有 1 个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数
规则 3
:如果有多个输入生命周期参数,但其中一个是&self
或&mut self
(是一个对象的方法:在结构体、枚举类型、trait对象中的函数被称为方法)
那么 self
的生命周期会被赋给所有输出生命周期参数
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 {
现在这个函数签名中的所有引用都有了生命周期,如此编译器可以继续它的分析而无须程序员显式的标记这个函数签名中的生命周期。
再次假设自己是编译器
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 {
'static
是一个特殊的生命周期,表示:整个程序的执行期
'static
生命周期,我们也可以选择像下面这样标注出来:'static
的。fn main() {
let s: &'static str = "I have a static lifetime.";
}
你可能在错误信息的帮助文本中见过使用 'static
生命周期的建议,不过将引用指定为 'static
之前,思考一下这个引用是否真的在整个程序的生命周期里都有效。你可能会考虑希望它一直有效,如果可能的话。大部分情况,代码中的问题是尝试创建一个悬垂引用或者可用的生命周期不匹配,请解决这些问题而不是指定一个 'static
的生命周期。
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>
这样