这是笔者学习rust的学习笔记(如有谬误,请君轻喷)
- 参考视频: https://www.bilibili.com/video/BV1hp4y1k7SV
- 参考书籍:rust程序设计语言:https://rust.bootcss.com/title-page.html
- markdown地址:https://github.com/fengyuan-liang/notes/blob/main/rust/rust基础.md
rust的优点有:高效、安全性、无所畏惧的并发
参考资料:
- rust程序设计语言:https://rust.bootcss.com/title-page.html
- rust权威指南:https://kaisery.github.io/trpl-zh-cn/foreword.html
常用命令:
cargo build
cargo run
cargo update
use rand::Rng; // trait 相当于java里的接口
use std::cmp::Ordering;
use std::io; // prelude
fn main() {
println!("猜数!!");
// 默认i32类型
let secret_number = rand::thread_rng().gen_range(0..=100);
println!("神秘数字是:{}", secret_number);
loop {
println!("猜测一个数");
let mut guess = String::new();
// io:Result Ok, Err err会返回失败的结果
io::stdin().read_line(&mut guess).expect("无法读取行");
// shadow
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
println!("你猜的数是:{}", guess);
match guess.cmp(&secret_number) {
Ordering::Less => println!("Too small!"), // arm
Ordering::Greater => println!("To big"),
Ordering::Equal => {
println!("You win!!");
break;
}
}
}
}
在rust里面声明变量使用的是let
关键字,默认情况下,变量是不可变的lmmutable
如果需要可变,可以添加mut
这个关键字
变量与常量
常量(constant),常量在绑定值之后也是不可变的,但是它与不可变的变量有很多区别
mut
,常量永远都是不可变的const
关键字,它的类型必须被标注常量在程序运行期间,其声名的作用域内一直有效;在rust
中常量使用全大写字母,每个单词之间使用下划线分割开,例如:MAX_POINTS
shadowing(隐藏)
fn main() {
let x = 5;
let x = x + 1;
let x = x + 1;
println!("x is {}", x) // x is 7
}
类型也可以不一样,如果使用mut是不可以的
fn main() {
let spaces = " ";
let spaces = spaces.len();
println!("spaces is {}", spaces) // spaces is 4
}
rust有四个主要的标量类型:
rust的整数类型如下图:
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 |
所谓的复合类型就是可以将多个值放在一个类型里面
rust提供了两种基础的复合类型:元组(Tuple)、数组
Tuple(元组)
fn main() {
// define Tuple
let tup: (i32, f64, u8) = (500, 6.4, 1);
// destructure tuple
let (x, y, z) = tup;
println!("{},{},{}", x, y, z) // 500,6.4,1
}
可以使用点标记法
来访问Tuple
里面的元素
fn main() {
// define Tuple
let tup: (i32, f64, u8) = (500, 6.4, 1)
println!("{},{},{}", tup.0, tup.1, tup.2) // 500,6.4,1
}
数组
fn main() {
let months = [
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December",
];
println!("{}", months[0]) // January
}
一般数组以[类型;长度]
来表示
let a: [i32; 5] = [1, 2, 3, 4, 5];
如果数组中的值都相等,也可以这样进行声明
fn main() {
let arr = [3; 5];
println!("{:?}", arr); // [3, 3, 3, 3, 3]
}
在rust中函数使用fn
关键字
按照惯例,针对函数和变量名,rust使用snake case
命名规范:所有字母都是小写,单词之间使用下划线分开
fn main() {
println!("hello world");
another_function()
}
// specification of snake case
fn another_function() {
println!("another function")
}
fn main() {
println!("hello world");
another_function(5) // argument
}
fn another_function(x: i32) { // parameter
println!("the value of x is {}", x)
}
let
将一个语句赋给一个变量这一块有一点玄学,得仔细看看
fn main() {
let y = {
let x = 1;
x + 3 // 没有加分号,`x + 3`是一个表达式,可以作为语句的一部分,表达式会计算产生一个值作为返回
};
println!("The value of y is {}", y) // The value of y is 4
}
如果我们加上分号,就变成语句
了,语句没有返回(其实准确一点,语句的返回值是一个空的Tuple
,也就是()
)
->
符号后边声明函数返回值的类型,但是不可以为返回值命名return
关键字,并且指定一个值fn main() {
let x = plus_five(6);
println!("The value of x is: {}", x) // The value of x is: 11
}
fn plus_five(x:i32) -> i32 {
5 + x
}
if表达式
运行您根据条件执行不同的代码分支else表达式
fn main() {
let number = 3;
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
}
当然也可以用我们之前学的match
来完成if分支
fn main() {
let number = 3;
match number {
n if n < 5 => {
println!("condition was true");
}
_ => {
println!("condition was false");
}
}
}
在rust中loop一共有三种:loop
、while
和for
loop
fn main() {
let mut cnt = 0;
let result = loop {
if cnt < 10 {
cnt += 1;
} else {
break cnt;
}
};
println!("The result is: {}", result) // The result is: 10
}
while
fn main() {
let mut cnt = 0;
while cnt < 10 {
cnt +=1
}
println!("The result is: {}", cnt) // The result is: 10
}
for
fn main() {
let arr = [10, 20, 30, 40, 50];
// arr.iter() Returns an iterator over the slice
for element in arr.iter() {
println!("the value is:{}", element)
}
}
range
在rust中可以使用..
符号表示这是一个可遍历的range
fn main() {
for number in (1..4).rev() {
println!("The value of number is: {}", number)
}
}
在rust中提供了一个极为强大的控制流运算符match
,由于match
和Option
合在一起更加好理解一些,所以可以看5.4小节
所有权是Rust
最独特的特性,它让Rust
无需GC
就可以保证内存安全
在rust中,一个值是在Stack
上还是在Heap
上对语言的行为和你为什么要做某些决定是有非常大的影响的
在我们的代码运行的时候,Stack&Heap的结构非常不同
Stack
按值的接受顺序来存储,按照相反的顺序将它们移除(后进先,LIFO)
- 添加数据叫做
压入栈
- 移除数据叫做
弹出栈
所有存储在
Stack
上的数据必须拥有已知的的固定的大小
- 编译时大小未知的数据或运行时大小可能发生变化的数据必须存放在
heap
上
Heap
内存组织性差一些:
- 当你把数据放入heap时,我们需要申请一定数量的空间
- 操作系统在heap内找到一块足够大的空间,把它标记为
在用
,并返回一个指针
,也就是这个空间的地址- 这个过程叫做在heap上进行分配,有时仅仅称为
分配
在stack和heap上分配空间并不相同
把值压到stack上不叫分配
因为指针是固定大小的,可以把指正存放到stack上,再通过指针来找数据
把数据压到stack上要比heap上分配快的多
- 因为操作系统不需要寻找用来存储新数据的空间,那个位置永远都在stack的顶端
在heap上分配空间需要做更多的工作
- 操作系统首先需要找到一个足够大的空间来存放数据,然后要做好记录方便下次分配
所有权解决的问题:
一旦我们理解了
所有权
,就不需要经常去想stack
或heap
了但是知道管理heap数据是所有权存在的原因,这有助于解释它为什么会这样工作
scope
时,该值将被删除Scope
就是程序中一个项目的有效范围,这里跟其他语言类似
fn main() {
// s不可用
let s = "hello"; // s 可用
// 可以对s进行相关操作
} // s 作用域到此结束,s不再可用
这里我们用rust中比较特殊的一种数据结构String
为例来研究研究
字符串字面值,在编译时就知道它的内容了,其文本内容直接被硬编码到最终的可执行文件中
- 这里的好处是速度快、高效(因为字符串的
不可变性
)
String
类型,为了支持可变性,需要在heap上分配内存来保证编译时未知的文本内容
- 操作系统必须在运行时来申请内存(通过
String::from
来实现)- 当用完
String
之后,需要使用某种方式将内存返回给操作系统- 如果是在有
GC
的语言中,GC
会跟踪并清理不再使用的内存- 没有
GC
,就需要我们去识别内存何时不再使用,并调用代码将它返回
- 如果忘了,就是浪费内存(内存泄露)
- 如果提前做了,变量就会变成非法的
- 如果做了两次,也会有问题。必须一次分配对应一次释放
rust采用了不同的方式:对于某个值来说,当拥有它的变量走出作用范围时,内存会立即自动的交还给操作系统
fn main() {
let mut str = String::from("Hello");
str.push_str(" world");
println!("{}", str) // Hello world
}
// 当程序执行完之后会自动的调用`drop()`来清理heap的内存
在其他语言中,如果这样做会出现double free
的bug
let mut s1 = String::from("Hello");
let s2 = s1;
在rust中为了保证内存安全,并没有尝试复制被分配的内容
s1
失效,当s1
离开作用域时,也就不需要释放任何东西了s1
,就会报错这里要补充两个概念
我们也许可能认为上述复制指针、长度、容量
是浅拷贝
,但由于rust
让s1
失效了,所以这里我们用一个新的术语移动(Move)
这里隐含的一个设计原则:Rust不会自动创建数据的深拷贝
当然如果我们不想进行move
,而想深拷贝的话可以使用clone
方法,此方法主要针对的是heap
上的数据
fn main() {
let s1 = String::from("Hello");
let s2 = s1.clone();
println!("{} {}", s1, s2)
}
在stack上的数据我们只需要进行复制
就可以了
fn main() {
// 标量在声明的时候就可以确定大小了,所以分配在stack上
// 对于这些值的操作永远都是非常快速的,所以没有深拷贝和浅拷贝的区别
let x = 5;
let y = x;
println!("{} {}", x, y)
}
在语义上,将值传递给函数和把值赋给变量是类似的
移动
或复制
fn main() {
let s = String::from("hello world");
// s的值进行了move,再后续作用域中就失效了
take_ownership(s);
let x = 5;
// x是i32类型的,实现了copy这个trait,所以这里传入的是x的副本
makes_copy(x);
println!("x:{}", x) // x:5
}
// 当some_string执行完后rust就会调用drop释放其内存
fn take_ownership(some_string: String) {
println!("{}", some_string)
}
fn makes_copy(some_number: i32) {
println!("{}", some_number)
}
函数在返回值的过程中同样也会发生所有权的转移
fn main() {
let s1 = gives_ownership();
println!("s1:{}", s1); // s1:hello worl
let s2 = String::from("hello");
println!("s2:{}", s2); // s2:hello
let s3 = takes_and_gives_back(s2);
// println!("s2:{}", s2); // s2已经move了,所以这里会报错
println!("s3:{}", s3); // s3:hello
}
// 返回值所有权进行移动
fn gives_ownership() -> String {
return String::from("hello world");
}
// move
fn takes_and_gives_back(a_string: String) -> String {
a_string
}
一个变量的所有权总是遵循同样的模式
heap
数据的变量离开作用域时,它的值就会被drop
函数清理,除非数据的所有权移动到另一个函数上了那么如何让函数使用某个值,但是不获得其所有权呢?
fn main() {
let s1 = String::from("hello");
// 第一种方法是将其所有权传入函数,再由函数返回值将所有权返回
let (s2, len) = calculate_length(s1);
println!("The length of '{}' is {}.", s2, len)
}
fn calculate_length(s:String) -> (String, usize) {
let length = s.len();
(s, length)
}
上面的方式太笨了,在rust中有一个特性
引用(Reference)
,可以解决上述的问题
在上述例子中我们可以使用rust提供的reference进行传递,这样所有权就不会进行转移
fn main() {
let s1 = String::from("hello");
// 第二种方式,传入的是引用
let len = calculate_length(&s1);
println!("The length of '{}' is {}.", s1, len) // The length of 'hello' is 5.
}
fn calculate_length(s: &String) -> usize {
let length = s.len();
length
}
具体来说
&String
而不是String
&
符号就表示引用:允许你引用某些值而不取得其所有权这里我们马上可以引出第二个概念借用
借用
那么我们是否可以修改借用的东西呢?答案是不行,引用
和变量
一样,默认是不可变的
我们可以给引用
添加mut
关键字让其可变
注意☢:可变引用有一个重要的限制,在特定作用域内,对某一块数据,只能有
一个可变的引用
这样的好处是可以在编译时防止数据竞争
但是以下三种行为下还是会发生数据竞争
当然我们也可以通过创建新的作用域,来允许非同时的创建多个可变引用
fn main() {
let mut s = String::from("Hello");
{
// s1的作用域和s2不一样,所以可以创建同一个变量的多个引用
let s1 = &mut s;
}
let s2 = &mut s;
}
注意☢:这里还有另外的一个限制
- 不可以同时拥有一个可变引用和一个不变的引用
- 多个不变的引用是可以的
悬空指针(Dangline Pointer):一个指针引用了内存中的某个地址,而这块内存可能已经释放并分配给其他人使用了
在Rust中,编译器可以保证引用永远都不是悬空引用
小结一下在rust中引用的规则
在rust中提供了一种不持有所有权的数据类型:切片(slice)
我们有一个案例来演示一下,在这个案例中我们要编写一个函数
fn main() {
let mut s = String::from("Hello world");
let worldIndex = first_world(&s);
println!("index {}", worldIndex) // index 5
}
fn first_world(s: &String) -> usize {
let bytes = s.as_bytes();
// enumerate会保证迭代器结果并保证为元组返回
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return i;
}
}
return s.len();
}
但是这样写其实是有一些问题的,因为当索引返回后我们修改了字符串,索引不会对应进行改变
fn main() {
let mut s = String::from("Hello world");
let worldIndex = first_world(&s);
s.clear(); // 清理字符串
println!("index {}", worldIndex) // index 5
}
针对这些问题rust提供了字符串切片
fn main() {
let mut s = String::from("Hello world");
// 字符串切片通过[开始索引..结束索引]
let hello = &s[0..5]; // 左开右闭 可以使用语法糖简写为 `&s[..5]`
let world = &s[6..11]; // 简写为 `&s[6..]` 还有 &s[..]; // 全切
println!("{}{}", hello, world)
}
我们使用字符串切片改写下上面的案例
fn main() {
let mut s = "Hello world ";
let worldIndex = first_world(s);
s.trim();
println!("{}", worldIndex);
}
// 返回值str表示返回一个切片
fn first_world(s: &str) -> &str {
let bytes = s.as_bytes();
// enumerate会保证迭代器结果并保证为元组返回
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[..i];
}
}
&s[..]
}
在上面的例子中我们使用s: &String
作为参数,对于有经验的rust开发者会使用&str
类型的参数
这样的好处是既可以传入String
也可以传入str切片
直接上例子
fn main() {
let u1 = User {
username: String::from("张三"),
email: String::from("[email protected]"),
sign_in_count: 556,
active: true,
};
println!("{:?}", u1); // 使用 {:?} 格式化标记,我们打印了结构体的调试表示
}
// 使用 #[derive(Debug)] 注解来自动实现 Debug trait
#[derive(Debug)]
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
当字段名和接收的参数名一样时可以简写
fn build_user(email: String, username: String) -> User {
User {
email, // 本来是email: email,可以简写为email
username,
active: true,
sign_in_count: 0,
}
}
注意:这里需要注意的一点是,
struct
里面的字段要么全部声明为可变的,要么全部不可变
下面的例子中,就是声明了不可变的struct
当我们想要基于某个struct实例来创建一个新实例的时候,可以使用struct更新语法
举个例子,下面的user2
里面的active
和sign_in_count
字段是和user1
里面一样
let u2 = User {
username: String::from("张三"),
email: String::from("[email protected]"),
sign_in_count: u1.sign_in_count,
active: u1.active,
};
这个时候我们就可以简写为
let u2 = User {
username: String::from("张三"),
email: String::from("[email protected]"),
..u1
};
我们可以定义类似Tuple
的Struct
,叫做Tuple struct
Tuple struct
整体有个名,但里面的元素没有名tuple
起名,并让它不同于其他tuple
,而且又不需要给每个元素起名我们可以这样定义Tuple struct
:struct关键字 名字 元素类型
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
注意:这里的black
和origin
是不同的类型,是不同的Tuple struct
实例
我们可以定义没有任何字段的struct,叫做Unit-Like struct
(因为与空元组()
,单元类型类似)
Unit-Like Struct
适用于需要在某个类型上实现某个trait
,但是在里面又没有想要存储的数据
我们的struct
struct User {
username: String,
email: String,
sign_in_count: u64,
active: bool,
}
这里的字段使用的是String
而不是&str
struct里也可以存放引用,但是需要使用到生命周期
小例子
我们使用struct来完成一个计算长方形面积的小例子
fn main() {
let rect = &Rectangle { width: 30, length: 50 };
println!("{}", area(rect));
// `{:?}` 打印简单的格式化内容 `{:#?}` 打印详细的格式化内容
println!("{:?}", rect); // Rectangle { width: 30, length: 50 }
}
#[derive(Debug)]
struct Rectangle {
width:u32,
length:u32,
}
// 长方形的长和宽是有关联的
fn area(rect: &Rectangle) -> u32 {
rect.width * rect.length
}
方法和函数类似:fn
关键字、名称、参数、返回值
方法和函数的不同:
struct(或enum、trait对象)
的上下文中定义self
,表示方法被调用的struct
实例那么如何定义方法呢?
impl
块里定义方法&self
,也可以获得其所有权或可变借用。和其他参数一样fn main() {
let rect = &Rectangle {
width: 30,
length: 50,
};
println!("{}", rect.area());
println!("{:?}", rect); // Rectangle { width: 30, length: 50 }
}
#[derive(Debug)]
struct Rectangle {
width: u32,
length: u32,
}
impl Rectangle {
// 长方形的长和宽是有关联的
fn area(&self) -> u32 {
self.width * self.length
}
}
方法调用的运算符
在rust中会自动引用和解引用,在调用方法时就会发生这种行为
在调用方法时,rust根据情况自动添加&
、&mut
或*
,以便object
可以匹配方法的签名
下面两行代码的效果是相同的
// 第一种
p1.distance(&p2);
// 第二种
(&p1).distance(&p2);
方法参数
方法可以有多个参数
fn main() {
let rect = &Rectangle {
width: 30,
length: 50,
};
let rect2 = &Rectangle {
width: 30,
length: 50,
};
let rect3 = &Rectangle {
width: 30,
length: 50,
};
println!("rect 可以容纳 rect2 {}", rect.can_hold(&rect2));
println!("rect 可以容纳 rect3 {}", rect.can_hold(&rect3));
}
#[derive(Debug)]
struct Rectangle {
width: u32,
length: u32,
}
impl Rectangle {
// 长方形的长和宽是有关联的
fn area(&self) -> u32 {
self.width * self.length
}
fn can_hold(&self, other:&Rectangle) -> bool {
self.width > other.width && self.length > other.length
}
}
可以在impl
块里定义不把self
作为第一个参数的函数,它们叫关联函数(不是方法)
String::from()
关联函数通常用于构造器
fn main() {
// 使用关联函数创建
let rect = Rectangle::square(20);
let rect2 = &Rectangle {
width: 30,
length: 50,
};
let rect3 = &Rectangle {
width: 30,
length: 50,
};
println!("rect 可以容纳 rect2 {}", rect.can_hold(&rect2));
println!("rect 可以容纳 rect3 {}", rect.can_hold(&rect3));
}
#[derive(Debug)]
struct Rectangle {
width: u32,
length: u32,
}
impl Rectangle {
// 长方形的长和宽是有关联的
fn area(&self) -> u32 {
self.width * self.length
}
fn can_hold(&self, other:&Rectangle) -> bool {
self.width > other.width && self.length > other.length
}
// 关联函数
fn square(size: u32) -> Rectangle {
Rectangle { width: size, length: size }
}
}
每个函数可以拥有多个
impl
块
我们直接看枚举的例子
fn main() {
// 使用枚举创建枚举值
let four = IpAddrKind::IPV4;
let six = IpAddrKind::IPV6;
println!("{:?}", four); // IPV4
println!("{:?}", six); // IPV6
}
// 我们将枚举类型里面的值称之为枚举的变体
#[derive(Debug)]
enum IpAddrKind {
IPV4,
IPV6
}
我们可以将枚举嵌入到结构体中
fn main() {
let addr1 = IpAddr::new(IpAddrKind::IPV4, String::from("127.0.0.1"));
println!("{:?}", addr1); // IpAddr { kind: IPV4, address: "127.0.0.1" }
}
// 我们将枚举类型里面的值称之为枚举的变体
#[derive(Debug)]
enum IpAddrKind {
IPV4,
IPV6
}
#[derive(Debug)]
struct IpAddr {
kind: IpAddrKind,
address: String
}
impl IpAddr {
fn new(kind: IpAddrKind, address: String) -> IpAddr {
IpAddr { kind: kind, address: address }
}
}
我们也可以将数据附加到枚举的变体中
这样做的好处有
struct
fn main() {
let addr1 = IpAddr::new(
IpAddrKind::IPV4(127, 0, 0, 1),
String::from("127.0.0.1")
);
println!("{:?}", addr1); // IpAddr { kind: IPV4(127, 0, 0, 1), address: "127.0.0.1" }
}
// 我们将枚举类型里面的值称之为枚举的变体
#[derive(Debug)]
enum IpAddrKind {
IPV4(u8, u8, u8, u8),
IPV6(String)
}
#[derive(Debug)]
struct IpAddr {
kind: IpAddrKind,
address: String
}
impl IpAddr {
fn new(kind: IpAddrKind, address: String) -> IpAddr {
IpAddr { kind: kind, address: address }
}
}
标准库里面的IpAddr
也是使用枚举实现的
枚举的方法和结构体的方法一样
// 我们将枚举类型里面的值称之为枚举的变体
#[derive(Debug)]
enum IpAddrKind {
IPV4(u8, u8, u8, u8),
IPV6(String),
}
impl IpAddrKind {
fn newIpV4(u1 :u8, u2: u8,u3: u8,u4: u8) -> IpAddrKind {
IpAddrKind::IPV4(u1, u2, u3, u4)
}
}
Prelude(预导入模块)
中我们知道rust中没有Null
,在其他语言中:
Null
是一个值,它表示没有值
空值(null)
、非空
Null的作者在2009年的一次演讲称Null引用是
Billion Dollar Mistake
文献链接
- Null的问题在于:当你尝试像使用非
Null
值那样使用Null
值的时候,就会引起某种错误(例如java中的NPE)- Null的概念还是有用的:因某种原因而变为无效或缺失的值
在Rust中提供了类似Null
概念的枚举——Option
在标准库中的定义是这样的
enum Option<T> {
Some(T),
None,
}
举个栗子叭
fn main() {
// 前两个值是有效的值
let some_number = Some(5);
let some_string = Some("A String");
// None表示这是一个无效的值
let absent_number: Option<i32> = None;
}
那么
Option
比Null
的设计好在哪呢?
Option
和T
是不同的类型,不可以把Option
直接当做T
进行使用- 如果想要使用
Option
中的T
,必须要将它转换为T
。这样我们就可以在转换的过程中处理为None
的情况,而不是像其他语言(例如Java的NPE)一样出现问题总结一下就是我们需要主动去处理
Null
的情况,不会出现Null值泛滥
的情况
在rust中提供了一个极为强大的控制流运算符match
match
允许一个值与一系列模式进行匹配,并执行匹配的模式对应的代码子面值
、变量名
、通配符
等等来个例子
fn main() {
println!("penny {}", value_in_cents(Coin::Penny)) // penny 1
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter,
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
Coin::Quarter => {
println!("多行的情况需要加上花括号");
25
},
}
}
绑定值的模式
匹配的分支可以绑定到被匹配对象的部分值,因此我们可以从enum
变体中获取值
fn main() {
println!("penny {}", value_in_cents(Coin::Quarter(UsState::Alabama))) // State quarter from Alabama\n penny 25
}
#[derive(Debug)]
enum UsState {
Alabama,
Alaska,
}
enum Coin {
Penny,
Nickel,
Dime,
Quarter(UsState),
}
fn value_in_cents(coin: Coin) -> u8 {
match coin {
Coin::Penny => 1,
Coin::Nickel => 5,
Coin::Dime => 10,
// 绑定值的模式匹配
Coin::Quarter(state) => {
println!("State quarter from {:?}", state);
25
},
}
}
在rust的match匹配中,必须要穷举所有的变体
但是如果match
要匹配的变体
太多了,我们不想处理那么多该怎么办呢?
_
来替代其余没列出的值fn main() {
let v = 0u8;
match v {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
// 其他的不想匹配了,可以使用`_`替代,下换线通配符只能放到最后一行
_ => println!("other")
}
}
上述的例子可以使用if let
控制流进行改写
fn main() {
let v = 0u8;
match v {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
// 其他的不想匹配了,可以使用`_`替代,下换线通配符只能放到最后一行
_ => println!("other"),
}
// 可以使用let进行简写
if let 3 = v {
println!("three");
} else {
println!("other")
}
}
小结一下if let
的作用
if let
当做是match
的语法糖一个与 if let
结构类似的是 while let
条件循环,它允许只要模式匹配就一直进行 while
循环。下面是展示了一个使用 while let
的例子,它使用 vector 作为栈并以先进后出的方式打印出 vector 中的值:
let mut stack = Vec::new();
stack.push(1);
stack.push(2);
stack.push(3);
while let Some(top) = stack.pop() {
println!("{}", top);
}
这个例子会打印出 3、2 接着是 1。pop
方法取出 vector 的最后一个元素并返回 Some(value)
。如果 vector 是空的,它返回 None
。while
循环只要 pop
返回 Some
就会一直运行其块中的代码。一旦其返回 None
,while
循环停止。我们可以使用 while let
来弹出栈中的每一个元素
fn main() {
let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);
// 使用模式匹配和Option可以避免其他语言出现NPE的问题
match six {
Some(value) => println!("six: {}", value),
None => println!("six: None"),
}
match none {
Some(value) => println!("none: {}", value),
None => println!("none: None"),
}
}
fn plus_one(x: Option<i32>) -> Option<i32> {
match x {
None => None,
Some(i) => Some(i + 1)
}
}
首先我们来看一下rust的代码组织
我们也将代码组织称为模块组织
,在rust中有以下的模块组织:
library
或可执行文件struct
、function
、或module
等项命名的方式首先我们来看一下
Package、Crate
的结构:crate的类型有:
- binary
- library
Crate root:
- 是源代码文件
- rust编译器从这里开始,组成你的
Crate
的根Module
一个Package可以有:
- 包含一个
Cargo.toml
,它描述了如何构建这些Crates
- 只能包含0-1个
library crate
- 可以包含任意数量的
Binary crate
- 但必须至少包含一个
crate (library 或 binary)
我们使用cargo
构建一个新的package
在生成的文件中目录结构如下:
$ tree
| .gitignore
| Cargo.toml
|
\---src
main.rs
在cargo中有一些惯例:
src/main.rs
binary crate
的crate root
src/lib.rs
library crate
src/main.rs
和src/lib.rs
src/bin
binary crate
Crate的作用
- 将相关的功能组合到一个作用域内,便于在项目间进行分享(防止命名的冲突)
- 例如我们有一个叫做
rand
的crate,访问它的功能需要通过它的名字rand
进行访问
定义module来控制作用域和私有性
Module的作用有:
- 在一个
crate
内,将代码进行分组- 增加可读性,易于复用
- 控制项目(item)的私有性,例如public、private
那么如何创建module
呢,其实使用mod
关键字就可以了
举个例子
mod front_of_house {
mod hosting {
fn add_to_waitlist(){}
fn seat_at_table(){}
}
mod serving {
fn take_order(){}
fn serve_order(){}
fn take_payment(){}
}
}
这些mod
是定义在lib.rs
这个crate
里面的,如果要看他们的层级结构,是这样的
crate(lib.rs)
|
\---front_of_house
|
\---hosting
| \---add_to_waitlist
| \---seat_at_table
\---serving
|
\---take_order
\---serve_order
\---take_payment
src/main.rs
和src/lib.rs
叫做crate roots
,这两个文件(任意一个)的内容形成了名为crate
的模块,位于整个模块树的根部
为了在rust的模块中找到某个条目,需要使用路径
路径有两种表示方式:
crate root
开始,使用crate名或字面值crateself
、super
或当前模块的标识符路径至少由一个标识符组成,标识符之间使用::
举个例子(下面的代码在lib.rs
这个crate下面):
mod front_of_house {
mod hosting {
fn add_to_waitlist(){}
fn seat_at_table(){}
}
mod serving {
fn take_order(){}
fn serve_order(){}
fn take_payment(){}
}
}
pub fn eat_at_restaurant() {
// 绝对路径从crate开始(优先选择)
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
front_of_house::hosting::add_to_waitlist();
}
如果我们build上面的代码其实是会报错的
这里引出一个知识点:私有边界(privacy boundary)
我们修改下上面的代码后编译就可以正常通过了
pub mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist(){}
fn seat_at_table(){}
}
mod serving {
fn take_order(){}
fn serve_order(){}
fn take_payment(){}
}
}
pub fn eat_at_restaurant() {
// 绝对路径从crate开始(优先选择)
crate::front_of_house::hosting::add_to_waitlist();
// 相对路径
front_of_house::hosting::add_to_waitlist();
}
使用pub关键字就可以将这些资源定义为公共的了
rust中属性大部分都是默认私有的,需要加上pub关键字来暴露
但是枚举只需要在枚举前加上pub其所有变体就都是公共的了
mod back_of_house { // 只需要在枚举定义时加上pub,其变体就都是pub的了 pub enum Appetizer { Soup, Salad } }
我们在文件系统里面经常使用..
来表示上级目录,在rust中使用的是super
关键字
super:用来访问父级模块路径中的内容,类似与文件系统中的..
fn serve_order() {}
mod back_of_house {
fn fix_incorrent_order() {
cook_order();
// 使用super关键字调用上级
super::serve_order();
// 如果是绝对路径
crate::serve_order()
}
fn cook_order(){}
}
use关键字可以将路径导入到作用域中
pub mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist(){}
fn seat_at_table(){}
}
}
// 使用use关键字,但此时use的内容只能在当前作用域内访问
use crate::front_of_house::hosting;
// 如果加上pub关键字后,相当于把use的内容导出了,其他作用域也可以使用 (重导出)
// pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
// 通过`use`导入可以直接使用
hosting::add_to_waitlist();
}
use的习惯用法
函数:将函数的父级模块引入作用域(指定到父级)
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1, 2);
}
struct、enum,其他:指定完整路径(指定到本身)
同名条目:指定到父级
as关键字可以为引入的路径指定本地的别名
use std::fmt;
use std::io;
fn f1() -> fmt::Result {}
fn f2() -> io::Result {}
可以使用as改写为
use std::fmt::Result;
use std::io::Result as ioResult;
fn f1() -> Result {}
fn f2() -> ioResult {}
use std::cmp::Ordering;
use std::io;
// 可以写成下面的形式
use std::{io, cmp::Ordering}
还有一种情况是同级目录
use std::io;
use std::io::Write;
// 可以写成下面的形式
use std::{self, write};
我们可以使用*
号将路径中所有的公共条目都引入到作用域(谨慎使用, 一般不用)
在这里我们主要学习以下几种常用的集合类型
Vec
叫做vector
fn main() {
// 创建vector
let v:Vec<i32> = Vec::new();
// 可以使用宏`vec!`来创建
let mut v = vec![1, 2, 3];
// 添加元素
v.push(4);
for ele in v.iter() {
println!("{}", ele) // 打印v中的元素
}
// 删除vector
// 当v离开作用域后,就会自动删除,堆里面的空间也会被回收
}
我们可以通过以下方法进行访问
fn main() {
// 可以使用宏`vec!`来创建
let v = vec![1, 2, 3];
// 使用索引访问元素,如果越界就会发生panic
let third = &v[2];
println!("The third element is {}", third);
// 也可以使用get来获取,越界会返回None这个变体,我们会进行处理
match v.get(2) {
Some(third) => println!("The third element is {}", third),
None => println!("There is no third element"),
}
}
所有权和借用规则
举个例子:
遍历
fn main() {
// 申明了一个可变的变量
let v = vec![1, 2, 3];
for ele in v {
println!("{}", ele)
}
}
我们也可以在遍历的过程中修改里面的值
fn main() {
// 申明了一个可变的变量
let mut v = vec![1, 2, 3];
for ele in &mut v {
// 解引用
*ele += 50;
}
// 再次打印
for ele in v {
print!("{} ", ele) // 51 52 53
}
}
我们知道vector只能存储同类型的数据,我们也知道枚举的变体是可以附加数据的
所以我们可以通过enum来让vector
来存储多种数据类型
fn main() {
// 通过可附加数据的枚举可以让vector存放不同的数据
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
println!("{:?}", row) // [Int(3), Text("blue"), Float(10.12)]
}
#[derive(Debug)]
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
在rust的核心语言层面,只有一个字符串类型:字符串切片str (或 &str)
字符串切片:是对存储在其他地方、utf-8编码的字符串的引用
我们来看看String
类型
标准库
而不是核心语言创建
fn main() {
// 可以使用String自带的方法创建
let s1 = String::from("hello world");
// 可以通过字符串切片进行转换
let s2 = "hello world".to_string();
println!("{}", s2);
}
更新
put_str()
:可以把一个字符串切片附加到String
fn main() {
// 可以使用String自带的方法创建
let mut s1 = String::from("hello");
let s2 = "world".to_string();
// push_str方法不会获得所有权,在后续作用域中还是可以使用
s1.push_str(&s2);
println!("{} {}", s1, s2); // helloworld world
}
push()
:可以将单个字符附加到String
fn main() {
let mut s1 = String::from("hell");
s1.push('o');
println!("{}", s1); // hello
}
+
:拼接字符串
fn main() {
let s1 = String::from("hello");
let s2 = String::from(" world");
// + 运算符前面要是一个String 后面是字符串切片或者字符串引用
let s3 = s1 + &s2;
println!("{}", s3); // hello world
// s1不能使用了
// 其实 + 运算符使用的是类似`fn add(self, s: &str) -> String {...}`的方法
// 那为什么我们传入字符串引用也可以呢,rust这里使用解引用强制转换(deref coercion), 保持了作用域
// 第一个参数并没有保存作用域,所以在`+`运算符之后使用就会报错
println!("{}", s1);
println!("{}", s2);
}
format!
:我们也可以使用这个宏来拼接字符串(推荐使用,更加灵活)
fn main() {
let s1 = String::from("a");
let s2 = String::from("b");
let s3 = String::from("c");
// let s3 = s1 + "-" + &s2 + "-" + &s3;
// println!("{}", s3);
// 我们可以使用宏format!,不会取得所有权
let s3 = format!("{}-{}-{}", s1, s2, s3);
println!("{}", s3); // a-b-c
println!("{}", s1); // a
println!("{}", s2); // b
}
String的内部结构:
String其实是对
Vec
的包装,所以也可以使用Vec的一些API,例如len()
方法
字节(Bytes)、标量值(Scalar Values)、字形簇(Grapheme Clusters)
在rust中有三种看待字符串的方式:
bytes
chars()
字母
):很复杂,标准库没有提供我们可以切割字符串,切割如果有问题就会panic
use std::collections::HashMap;
fn main() {
// 创建
let mut scores = HashMap::new();
// 也可以使用两个Vector创建
// let kVec = vec![String::from("blue"), String::from("yellow")];
// let vVec= vec![10, 50];
// let scores2 = kVec.iter().zip(vVec.iter()).collect();
// 增
let blue = String::from("blue");
// 增
// 如果传String,其所有权就会改变,所以这里借用所有权
scores.insert(&blue, 10);
// 查 返回的是option
let score = scores.get(&blue);
match score {
Some(s) => println!("the score of blue is {}", s), // 10
None => println!("cannot find score"),
}
// 遍历hashMap
for (k, v) in &scores {
println!("k:{} v:{}", k, v) // k:blue v:10
}
// 改 如果传入同样的k,后一次会覆盖前面的value
scores.insert(&blue, 100);
match scores.get(&blue) {
Some(v) => println!("{}", v), // 100
_ => println!("cannot find"),
}
// 删
scores.remove(&blue);
match scores.get(&blue) {
Some(v) => println!("{}", v),
_ => println!("cannot find"), // cannot find
}
}
我们在插入的时候如果传入同样的k,后一次会覆盖前面的value
如果我们想要只在K
不对应任何值的情况下,才插入V
,可以使用entry
方法
entry方法:检查指定的
K
是否对应一个V
- 参数为
K
- 返回
enum Entry
:表示值是否存在
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(100);
for (k, v) in scores {
println!("k:{}, v:{}", k, v) // k:yellow, v:50 k:blue, v:10
}
}
我们分解一下上述过程,然后打印一下 scores.entry
的值
我们可以看到entry
的两个变体VacantEntry
和OccupiedEntry
,这两个变体执行的逻辑并不一样
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert(String::from("blue"), 10);
let entry = scores.entry(String::from("yellow"));
println!("entry:{:?}", entry); // entry:Entry(VacantEntry("yellow"))
entry.or_insert(50);
// scores.entry(String::from("blue")).or_insert(100);
let entry = scores.entry(String::from("blue"));
println!("entry:{:?}", entry); // entry:Entry(OccupiedEntry { key: "blue", value: 10, .. })
entry.or_insert(100);
for (k, v) in scores {
println!("k:{}, v:{}", k, v) // k:yellow, v:50 k:blue, v:10
}
}
Entry
的or_insert()
方法返回:
- 如果
K
存在,返回到对应的V
的一个可变引用- 如果
K
不存在,将方法参数作为K
的新值插入,返回到这个值的可变引用
我们来看个例子
use std::collections::HashMap;
fn main() {
let text = "hello world wonderful world";
let mut map = HashMap::new();
// split_whitespace 根据空格分割
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0);
// 如果存在会返回value的可变引用 所以这里我们可以对value进行操作
*count += 1;
}
println!("{:?}", map) // {"world": 2, "hello": 1, "wonderful": 1}
}
在默认情况下,hashMap
使用加密功能强大的hash函数
,可以抵抗DOS
攻击
我们可以通过指定不同的hasher
来切换到另一个函数,这里的hasher
是实现了BuildHasher trait
的类型
Rust的可靠性很大程度上是依靠rust强大的错误处理来完成的
在Rust中错误可以分为两类:
在Rust中没有类似异常捕获的机制
Result
panic!
进行处理当panic!
宏执行的时候,会发生:
我们为了应对panic,可以展开或者中止(abort)调用栈
默认情况下档panic发生时,我们可以选择
如果我们想要让二进制文件更小,可以设置将
展开
改为中止
- 在
Cargo.toml
中设置profile
的设置:panic = 'abort'
这里我们直接来一波测试
fn main() {
panic!("crash and burn")
}
如果这个panic!
不是发生在我们写的代码中,并且我们想要查看详细的堆栈信息的话可以设置环境变量来查看panic!的回溯信息
来查看
测试代码:
fn main() {
let arr = vec![1,2,3];
arr[1000];
}
设置环境变量并且运行
RUST_BACKTRACE=1
为了获取带有调试信息的回溯,必须启用调试符号(不带 --release
)
通常我们处理的错误都不会引起panic
,针对这种错误,我们可以使用Result
枚举处理
enum Result<T, E> {
Ok(T),
Err(E),
}
我们可以使用match表达式
来处理
use std::{fs::File, io::ErrorKind};
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Error creating file: {:?}", e)
}
ohter_error => panic!("Error opening the file: {:?}", ohter_error)
}
};
}
上面的代码中我们使用许多match
来处理错误,这种方式其实比较麻烦并且原始,我们可以闭包closure
来简化
Result
有很多方法,它们接受闭包作为参数我们改良一下上面的代码
use std::{fs::File, 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!("Error creating file: {:?}", error))
} else {
panic!("Error opening file: {:?}", error)
}
});
}
在上面我们就使用unwrap
来简化了match
操作
unwrap
的作用其实就是如果Result
返回的结果是Ok
就执行Ok
里面的部分,如果不是就会panic
但是这样也不好,因为panic
的信息可能并不是我们想要的,这个时候就可以使用expect
来自定义信息
use std::fs::File;
fn main() {
let f = File::open("test.txt").expect("无法打开文件");
}
我们有的时候遇到了错误,但是不想自己处理,而是想要给程序调用者进行处理,这个时候就要用到传播错误
我们可以手动进行错误的传递
use std::{fs::File, io::{self, Read}};
fn main() {
let r = read_username_from_file();
}
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(_) => Ok(s),
Err(e) => Err(e)
}
}
上面的代码我们也可以进行简写
use std::{fs::File, io::{self, Read}};
fn main() {
let r = read_username_from_file();
}
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)
}
这里?
的作用我们总结一下:
Result
是Ok
:Ok
中的值就是表达式的结果,然后继续执行程序Result
是Err
:Err
就是整个函数
的返回值,就像使用了return
一样其实上面?
运算符隐式的使用了from
函数
Trait std::convert::From::from(value)
函数被用于错误的转换?
所处理的错误,会隐式的被from
函数所处理?
调用from
函数时,它所接收的错误类型会被转化为当前函数返回类型所定义的错误类型上述的代码我们还可以进行链式调用
use std::{
fs::File,
io::{self, Read},
};
fn main() {
let r = read_username_from_file();
}
// 进行链式调用
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)
}
最后一点需要注意的是:
?
运算符只能处理Result
的类型
在main
方法中也可以使用?
运算符,只需要改写一下返回值就行
use std::{fs::File, error::Error};
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt")?;
Ok(())
}
这里Box
是trait
对象,可以简单理解为:任何可能的错误类型
总体原则:
Result
panic!
泛型在很多语言里都有,主要是为了提高抽象代码的能力,我们来看一段代码,比如我要找出Vector
里面的最大值
fn main() {
let arr1 = vec![1,4,5,6,7,7];
match max(&arr1) {
Some(max_num) => println!("max num is {}", max_num), // max num is
None => println!("cannot find")
}
let arr2 = vec!['a', 'u', 's', 'e'];
match max(&arr2) {
Some(max_num) => println!("max num is {}", max_num), // max num is u
None => println!("cannot find")
}
}
// 泛型限制符,传入的类型必须实现这两个trait
fn max<T: PartialOrd + Copy>(arr: &Vec<T>) -> Option<T> {
if arr.is_empty() {
return None;
}
let mut max_num = arr[0];
for &ele in arr.iter() {
if ele > max_num {
max_num = ele;
}
}
Some(max_num)
}
在结构体中定义泛型
fn main() {
let p1 = Point { x: 5, y: 5 };
let p2 = Point { x: 5.5, y: 5.5 };
}
struct Point<T> {
x: T,
y: T,
}
在枚举中定义泛型
我们在之前已经见过很多枚举使用泛型的案例了
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
方法定义中的泛型
为struct
或enum
实现方法的时候,可在定义中使用泛型
注意:
T
放在impl
关键字后,表示在类型T
上实现方法:例如 impl Point
impl Point
此外:
struct
里泛型类型参数可以和方法的泛型类型参数不同
fn main() {
let p1 = Point { x: 5, y: 5 };
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
}
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,
}
}
}
泛型代码的性能
在rust中使用泛型不会影响性能,原因是rust使用的单态化(monomorphization)
,在编译时就将泛型替换为具体的类型了
fn main() {
let artice = NewsArticle {
headline:String::from("headline"),
location: String::from("location"),
author: String::from("author"),
content: String::from("content"),
};
println!("NewsArticle: {} ", artice.summarize()) // NewsArticle: headline, by author (location
}
pub trait Summary {
fn summarize(&self) -> String;
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
// 实现trait
impl Summary for NewsArticle {
fn summarize(&self) -> String {
format!("{}, by {} ({})", self.headline, self.author, self.location)
}
}
可以在某个类型上实现某个trait
的前提条件是:这个类型或这个trait
是在本地crate
里定义的
无法为外部类型来实现外部的trait
:
一致性
)孤儿规则
:之所以这样命名是因为父类型不存在crate
可以为同一类型实现同一个trait
,Rust就不知道应该使用那个实现了我们可以在trait中默认进行实现,只要不重写该方法,就可以使用默认方法
fn main() {
let artice = NewsArticle {
headline:String::from("headline"),
location: String::from("location"),
author: String::from("author"),
content: String::from("content"),
};
println!("NewsArticle: {} ", artice.summarize()) // NewsArticle: default imp
}
pub trait Summary {
fn summarize(&self) -> String {
String::from("default impl")
}
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
// 实现trait
impl Summary for NewsArticle {
// 使用默认实现
// fn summarize(&self) -> String {
// format!("{}, by {} ({})", self.headline, self.author, self.location)
// }
}
这里一共有两种情况
第一种:impl trait
语法,适用于简单情况
fn main() {
let artice = NewsArticle {
headline:String::from("headline"),
location: String::from("location"),
author: String::from("author"),
content: String::from("content"),
};
println!("NewsArticle: {} ", artice.summarize()); // NewsArticle: default imp
summarize(artice) // default impl
}
pub trait Summary {
fn summarize(&self) -> String {
String::from("default impl")
}
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
// 实现trait
impl Summary for NewsArticle {
// 使用默认实现
// fn summarize(&self) -> String {
// format!("{}, by {} ({})", self.headline, self.author, self.location)
// }
}
// `impl trait`语法,适用于简单情况
fn summarize(s: impl Summary) {
println!("{}", s.summarize())
}
第二种:Trait Bound语法
,可用于复杂情况。其实impl trait
是Trait Bound
的语法糖
fn summarize<T: Summary>(s: T) {
println!("{}", s.summarize())
}
使用+
可以约束必须实现多个Trait
fn summarize<T: Summary + Display>(s: T) {
println!("{}", s.summarize())
}
如果我们Trait
约束比较多,我们就可以使用where
来简化
fn summarize<T: Summary + Display, U: Clone + Dubug>(s: T, b: U) {
println!("{}", s.summarize())
}
简化为
fn summarize<T, U (s: T, b: U)
where
T:Summary + Display,
U: Clone + Debug
{
println!("{}", s.summarize())
}
我们可以约束返回类型必须实现Trait,但是如果出现分支语句就可以能报错
这样是可以的
fn summarize<T: Summary>(s: T) -> impl Summary {
println!("{}", s.summarize());
NewsArticle {
headline:String::from("headline"),
location: String::from("location"),
author: String::from("author"),
content: String::from("content"),
}
}
这样是不行的,返回的类型必须是确定的,是同一种类型
在使用泛型参数的impl
块上使用Trait Bound
,我们可以有条件的为实现了特定Trait
的类型来实现方法
struct Pair<T> {
x: T,
y:T
}
impl <T> Pair<T> {
fn new(x: T, y: T) -> Self {
Self {x, y}
}
}
// 使用`cmp_display`方法,传入的参数必须实现这两个trait
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)
}
}
}
为满足Trait Bound
的所有类型上实现Trait
叫做覆盖实现blanket implementations
例如所有的数字都实现了Display
这个Trait
,所以可以使用to_string
方法
fn main() {
let str = 2.to_string();
}
在rust中每个引用都有自己的生命周期
生命周期:引用保持有效的作用域
大多数情况:生命周期是隐式的、可被推断的
当引用的生命周期可能以不同的方式互相关联时:需要手动标注生命周期
这里我们得知道生命周期存在的意义是为了避免 悬垂引用(dangling reference)
我们来看一个关于生命周期的例子
这里就是因为x
的生命周期没有r
长所导致的
生命周期的标注不会改变引用的生命周期长度
当指定了泛型生命周期参数,函数可以接受带有任何生命周期的引用
生命周期的标注:描述了多个引用的生命周期间的关系,但不会影响生命周期
生命周期参数名:
- 以
'
开头- 通常全小写且非常短
- 很多人使用
'a
生命周期标注的位置:
- 在引用的
&
符号- 使用空格将标注和引用类型分开
我们来看一些例子
&i32 // 一个引用 &'a i32 // 带有显式生命周期的引用 &'a mut i32 // 带有显式生命周期的可变引用
这里我们需要注意的是:单个生命周期标注本身没有意义
我们来看一个例子
上面的代码我们只需要添加上生命周期的标注即可
fn main() {
let s1 = String::from("abcd");
let s2 = "xyz";
let result = longest(s1.as_str(), s2);
println!("The longest string is {}", result);
}
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
这里需要注意的是,rust默认返回的值的生命周期是参数中生命周期较短的那个
比如下面的代码就会报错,原因是s2
生命周期较短,但是在作用域后还可能会调用
让我们深入进行理解生命周期
我们举个悬垂引用
的例子,当我们返回的引用指向的内存已经被清理的时候,当然就会出问题
针对上面的代码我们可以直接返回变量本身,将所有权转移出去即可
struct里可以包括
输入、输出生命周期
生命周期在:
编译器使用三个规则在没有显式标注生命周期的情况下,来确定引用的生命周期
fn
定义和impl
块规则一:每个引用类型的参数都有自己的生命周期
规则二:如果只有一个输入生命周期参数,那么该生命周期被赋给所有的输出生命周期参数
规则三:如果有多个输入生命周期参数,但其中一个是
&self
或&mut self
(是方法),那么self
的生命周期会被赋给所有的输出生命周期参数
我们针对上面的三条规则来举几个例子,这里需要假设我们是编译器
// 原代码
fn first_world(s: &str) -> &str { ... }
// 编译器运用第二条规则 添加输入生命周期
fn first_world<'a>(s: &'a str) -> &str { ... }
// 编译器运用第三条规则,添加输出生命周期
fn first_world<'a>(s: &'a str) -> &'a str { ... }
// 这样就是没啥问题的 编译通过
我们再来看个例子
// 原代码
fn longest(x: &str, y: &str) -> &str { ... }
// 编译器运用第一条规则,给每个参数都添加了生命周期
fn longest<'a, 'b>(x: &'a str, y: &'b str) -> &str { ... }
在struct
上使用生命周期实现方法,语法和泛型参数的语法一样
在哪声明和使用生命周期参数,依赖于:
struct字段的生命周期名:
impl
后声明struct
名后使用struct
类型的一部分impl块内的方法签名中:
引用
必须绑定与struct字段引用的生命周期,或者引用
是独立的也可以fn main() {
let novel = String::from("Call me Ishmael, Some years age...");
let first_sentence = novel.split('.')
.next()
.expect("Could not found a 'a'");
let i = ImportantExcerpt{ part: first_sentence };
}
struct ImportantExcerpt<'a> {
part: &'a str,
}
impl<'a> ImportantExcerpt<'a> {
// 根据第一条省略规则可以不为self添加省略规则
fn level(&self) -> i32 {
3
}
// 根据第一条规则为self添加生命周期
// 根据第三条规则,当输入生命周期有self时,输出生命周期将拥有self的生命周期
fn announce_and_return_part(&self, annoucement: &str) -> &str {
println!("Attention please: {}", annoucement);
self.part
}
}
static
是一个特殊的生命周期:整个程序的持续时间
例如:所有字符串字面值都拥有static
生命周期
let s: &'static str = "I have a static lifetime"
为引用指定static
生命周期前要三思:是否需要引用在程序整个生命周期内都存活
我们来看一个泛型参数类型、Trait Bound、生命周期的例子
fn longest_with_an_announcement<'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
}
}
测试通常需要执行三个操作(3A操作 Arrange, Act, Assert)
测试函数需要使用test
属性(attribute)进行标注
#[test]
,可以把函数变成测试函数cargo test
命令来运行所有测试函数Library
项目,默认会有一个test module
,当然我们可以自己再进行添加举个例子
$ cargo new test-project --lib
Created library `test-project` package
$ cd test-project/src/
$ cat lib.rs
pub fn add(left: usize, right: usize) -> usize {
left + right
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = add(2, 2);
assert_eq!(result, 4);
}
}
$ cargo test
Compiling test-project v0.1.0 (E:\workspacesJ2SE_VSCode\rust\rustStudy\test-project)
Finished test [unoptimized + debuginfo] target(s) in 2.50s
Running unittests src\lib.rs (target\debug\deps\test_project-9a1280a4860686ce.exe)
running 1 test
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests test-project
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
我们可以使用should_panic
属性来验证待测试 代码是否会panic,如果panic则测试通过
可以在属性中添加期待的panic信息
如果不会发生panic,可以使用Result
作为返回类型编写测试
我们可以通过添加测试参数来控制测试的行为
cargo test --help // 查看帮助
cargo test -- --help // 查看可以在--后的参数 针对二进制文件使用 -- --的形式
运行多个测试,默认使用多个线程并行运行,要确保好线程安全性
cargo test -- --test-threads=1 // 控制运行线程的为1
默认测试通过是不会打印println!
里面的内容,如果测试失败会看到里面的内容
cargo test -- --show-output // 成功的例子中也要打印
cargo test 测试名称 // 指定测试用例 只能指定一个
cargo test test // 会执行所有以 test开头的测试用例
忽略测试
只执行ignore的测试
cargo test -- --ignored // 注意有个d
在rust中测试分为单元测试和集成测试
在rust中集成测试完全位于被测试库的外部
目的:是测试被测试库的 多个部分是否能正确的一起工作
cargo test
的时候才会编译闭包:可以捕获其所在环境的匿名函数
- 是匿名函数
- 保存为变量、作为参数
- 可在一个地方创建闭包,然后在另一个上下文中调用闭包来完成运算
- 可从其定义的作用域捕获值
为了搞懂闭包的作用,我们来看一段例子
fn main() {
let add = |a, b| a + b;
let result = add(2, 3);
println!("Result: {}", result);
}
这里的闭包省略的返回值,因为编译器会自动进行返回,但是这种绑定是一次性的,如果下次再传入不一样类型的值就会报错
这里我们来一个小案例(运动计划)来看看闭包的作用以及一些使用的最佳实践,这里我们也是通过闭包
来实现了一个运动计划的小案例
use std::{thread, time::Duration};
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(simulated_user_specified_value, simulated_random_number);
}
fn generate_workout(intensity:u32, random_number:u32) {
// 闭包
let expensive_closure = |num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
};
if intensity < 25 {
// 再后续编译器可以推断出闭包能够返回的类型
println!("Today, do {} pushups!", expensive_closure(intensity));
println!("Next, do {} situps!", expensive_closure(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated");
} else {
println!("Today, run for {} minutes", expensive_closure(intensity));
}
}
}
在我们上面运动计划的小案例中,我们看到闭包其实被调用了两次,执行了多次比较耗时的操作
其实我们可以创建一个struct
,它持有闭包及其调用结果(第一次调用的时候就缓存起来)
这种模式通常叫做记忆化 (memoization)
或延迟计算(lazy evaluation)
如果让struct持有闭包
- struct的定义需要知道所有字段的类型,需要指明闭包的类型
- 每个闭包实例都有自己唯一的匿名类型,即使两个闭包签名完全一样
- 所以需要使用:泛型和
Trait Bound
在标准库中提供了许多Fn Trait
,所有的闭包都至少实现了以下Trait
之一:
我们这里修改一下上面的案例
use std::{thread, time::Duration};
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(simulated_user_specified_value, simulated_random_number);
}
struct Cacher<F>
where
F: Fn(u32) -> u32,
{
calculation: F,
value: Option<u32>,
}
impl<F> Cacher<F>
where
F: Fn(u32) -> u32,
{
fn new(calculation: F) -> Cacher<F> {
Cacher {
calculation,
value: None,
}
}
fn value(&mut self, arg: u32) -> u32 {
match self.value {
Some(v) => v,
None => {
let v = (self.calculation)(arg);
self.value = Some(v);
v
}
}
}
}
fn generate_workout(intensity: u32, random_number: u32) {
// 闭包
let mut expensive_closure = Cacher::new(|num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
});
if intensity < 25 {
// 再后续编译器可以推断出闭包能够返回的类型
println!("Today, do {} pushups!", expensive_closure.value(intensity));
println!("Next, do {} situps!", expensive_closure.value(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated");
} else {
println!(
"Today, run for {} minutes",
expensive_closure.value(intensity)
);
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn call_with_defferent_values() {
let mut c = super::Cacher::new(|a| a);
let v1 = c.value(1);
let v2 = c.value(2);
assert_eq!(v2, 2)
}
}
但是如果我们执行test会发现会失败,因为我们没有比较传入的值,而是直接将结果缓存
现在我们用HashMap
进行优化
use std::{thread, time::Duration, collections::HashMap, hash::Hash};
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(simulated_user_specified_value, simulated_random_number);
}
struct Cacher<F>
where
F: Fn(u32) -> u32,
{
calculation: F,
value: HashMap<u32, u32>,
}
impl<F> Cacher<F>
where
F: Fn(u32) -> u32,
{
fn new(calculation: F) -> Cacher<F> {
Cacher {
calculation,
value: HashMap::new(),
}
}
fn value(&mut self, arg: u32) -> u32 {
match self.value.get(&arg) {
Some(v) => *v,
None => {
let v = (self.calculation)(arg);
self.value.insert(arg, v);
v
},
}
}
}
fn generate_workout(intensity: u32, random_number: u32) {
// 闭包
let mut expensive_closure = Cacher::new(|num| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
});
if intensity < 25 {
// 再后续编译器可以推断出闭包能够返回的类型
println!("Today, do {} pushups!", expensive_closure.value(intensity));
println!("Next, do {} situps!", expensive_closure.value(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated");
} else {
println!(
"Today, run for {} minutes",
expensive_closure.value(intensity)
);
}
}
}
#[cfg(test)]
mod tests {
#[test]
fn call_with_defferent_values() {
let mut c = super::Cacher::new(|a| a);
let v1 = c.value(1);
let v2 = c.value(2);
assert_eq!(v2, 2)
}
}
闭包可以访问定义它作用域内的变量,函数不可以
fn main() {
let x = 4;
let equal_to_x = |z| z == x;
let y = 4;
assert!(equal_to_x(y)); // true
}
闭包从所在环境捕获值的方式有(与函数获得参数的三种方式一样):
创建闭包时,通过闭包对环境值的使用,Rust推断出具体使用哪个Trait
FnOnce
FnMut
Fn
所有实现了Fn
的都实现了FnMut
,所有实现了FnMut
的都实现了FnOnce
,即FnOnce{ FnMut { Fn } }
move关键字
在参数列表前使用
move
关键字,可以强制闭包取得它所使用的环境值的所有权
- 当将闭包传递给新县城以移动数据使其归新线程所有时,此技术最为有用
我们来看一下最佳实践
Fn trait bound
之一时,首先用Fn
,基于闭包体的情况,如果需要FnOnce
或FnMut
,编译器会再告诉你迭代器:对一系列执行某些任务
迭代器负责:
Rust的迭代器
所有的迭代器都实现了Iterator trait
,Iterator trait
定义于标准库,定义大致如下:
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
type Item
和Self::Item
定义了与此该trait
关联的类型,实现Iterator trait
需要定义一个Item
类型,它用于next
方法的返回类型(迭代器的返回类型)
Iterator trait
仅要求实现一个方法:next
Some
里None
Next
方法#[test]
fn iterator_demonstration() {
let v1 = vec![1, 2, 3];
let mut v1_iter = v1.iter();
assert_eq!(v1_iter.next(), Some(&1));
assert_eq!(v1_iter.next(), Some(&2));
assert_eq!(v1_iter.next(), Some(&3));
}
for in
循环其实是语法糖,本质也是通过获取到迭代器的所有权,然后通过match result
,直到遇到None
,IntoIterator
是个tarit
,它的into_iter
方法会取得 for .. in ..
中 in
右边的东西
我们总结一下这几种迭代方法
iter
方法:在不可变引用上创建迭代器into_iter
方法:创建的迭代器会获得所有权
在标准库中,Iterator trait
有一些带默认实现的方法,其中有一些方法会调用next
方法
Iterator trait
时必须实现next
方法的原因之一next
的方法叫做消耗性适配器
定义在Iterator trait
上的另外一些方法叫做迭代器适配器
,即 把迭代器转换为不同种类的迭代器
可以通过链式调用使用多个迭代器是配置来执行复杂的操作,这种调用可读性较高
例如:map
#[test]
fn iterator_sum() {
let v1 = vec![1, 2, 3];
// collect就是一个消耗性的方法
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
println!("{:?}", v2) // [2, 3, 4]
}
我们自定义一个迭代器并且实现next
方法
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Counter {
Counter { count: 0 }
}
}
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
if self.count < 5 {
self.count += 1;
Some(self.count)
} else {
None
}
}
}
#[test]
fn calling_next_directly() {
let mut counter = Counter::new();
assert_eq!(counter.next(), Some(1));
assert_eq!(counter.next(), Some(2));
assert_eq!(counter.next(), Some(3));
assert_eq!(counter.next(), Some(4));
assert_eq!(counter.next(), Some(5));
assert_eq!(counter.next(), None);
}
#[test]
fn using_other_iterator_trait_methods() {
let sum: u32 = Counter::new()
.zip(Counter::new().skip(1))
.map(|(a, b)| a * b)
// 2、6、12、20
.filter(|x| x % 3 == 0)
// 6、12
.sum();
assert_eq!(18, sum);
}
迭代器其实在底层编译之后会编程for循环的形式,我们将其称之为零开销抽象(Zero-Cost Abstraction)
,即使用抽象时不会引入额外的运行时开销
在这里我们主要学习:
release profile
来自定义构建https://crates.io
上发布库workspaces
组织大工程https://crates.io/
来安装库cargo
release profile:
每个profile
的配置都独立于其他的profile
Cargo主要的两个profile:
- dev profile:适用于开发,
cargo build
- release profile:适用于发布,
cargo build --release
那么如何自定义profile呢?
xxx profile
的配置,可以在Cargo.toml
里添加[profile.xxxx]
区域,在里面覆盖默认配置的子集更多的命令可以在这里看到:https://doc.rust-lang.org/stable/cargo/
我们先了解一下rust的文档注释
使用///
进行注释,举个例子
我们可以使用cargo doc
命令生成文档,它会运行rustdoc
工具(rust安装包会自带)
$ cargo doc --open
Documenting rustDemo01 v0.1.0 (E:\workspace\vscode\rustStudy\rustDemo01)
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
Opening E:\workspace\vscode\rustStudy\rustDemo01\target\doc\rustDemo01\index.html
上面的例子中# Examples
表示的是章节,还有几个其他的常用章节:
panic
的场景Result
,描述可能的错误种类,以及可导致错误的条件unsafe
调用,就应该解释函数unsafe
的原因,以及调用者确保的使用前提文档注释作为测试:
示例代码块的附加值:
- 运行
cargo test
:将把文档注释中的示例代码作为测试来运行再上面的例子中我们就写过一个
Example
,当我们cargo test
的时候就会执行里面的测试的代码
使用符号//!
来对crate进行描述注释(注意这种注释只能出现在最前面)
我们再生成文档
我们来看下下面的注释生成的文档
这个时候我们就可以使用pub use
将文档导出到首页(其实就是简化了路径)
发布crate前,需要先在crates.io
创建账号并获得API token
运行命令cargo login [你的API token]
,这个命令会通知cargo
,将你的API token 存储在本地~/.cargo/credientials.toml
上
在发布crate
之前,需要在cargo.toml
的[package]
区域为crate
添加一些元数据
http://spdx.org/licenses/
查找),可以使用OR
指定多个使用cargo publish
命令发布
注意:crate一旦发布,就是永久性的,该版本无法覆盖,代码也无法删除(为了让依赖该版本的项目可以继续正常工作)
可以使用
cargo yank --vers 1.0.1
命令标记当前版本不可使用(之前依赖的还可以继续使用,但是新创建的依赖就不能依赖了),可以使用cargo yank --vers 1.0.1 --undo
命令取消撤回
创建工作空间的方式有很多种,我们来举个创建一个二进制crate,两个库crate的例子
我们可以使用cargo install
来安装在cargo.io
上的binary crate
并且进行执行,并且可以添加到我们的环境变量中
智能指针是这样一些数据结构:
引用计数(reference counting)智能指针类型
引用和智能指针的其他不同
- 引用:只借用数据
- 智能指针:很多时候拥有它所指向的数据
智能指针的例子:
Vec
UTF-8
编码)智能指针通常使用struct
实现,并且实现了Deref
和Drop
这两个trait
Box
Box
举个例子
fn main() {
// 使用box在堆上分配内存
let b = Box::new(5);
println!("b = {}", b)
} // b的生命周期在作用域结束后也会结束,在堆上的空间会被释放掉
使用Box
Cons list
)关于Cons list
lisp
语言的一种数据结构Nil
值,没有下一个元素举个例子
那我们用Box优化一下
实现Deref Trait
使我们可以自定义解引用运算符*
的行为,通过实现Deref Trait
,智能指针可以像常规引用一样来处理
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
fn main() {
let x = 5;
let y = Box::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
定义自己的智能指针
Boxtuple struct
,我们现在定义一个MyBox
,也是拥有一个元素的tuple struct
标准库中的Deref trait
要求我们实现一个deref
方法:
self
use std::ops::Deref;
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // 等价于 `*(y.deref())`
}
struct MyBox<T>(T);
impl <T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
impl<T> Deref for MyBox<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
函数和方法的隐式解引用转化(Deref Coercion)
T
实现了Deref trait
,Deref Coercion
可以把T
的引用转化为T
经过Deref
操作后生成的引用Deref Coercion
就会自动发生deref
进行一系列调用,来把它转为所需的参数类型解引用与可变性
DerefMut trait
重载可变引用的*
运算符deref coercion
:
T:Deref
,允许&T
转换为&U
T:DerefMut
,允许&mut T
转换为&mut U
T:Deref
,允许&mut T
转换为&mut U
实现Drop Trait,可以让我们自定义当值将要离开作用域时发生的动作
Drop trait
Drop trait只要求你实现drop方法
我们看个例子:
fn main() {
let c = CustomSmartPointer {data: String::from("my stuff") };
let d = CustomSmartPointer {data: String::from("other stuff")};
println!("CustomSmartPointers created.")
}
struct CustomSmartPointer {
data: String
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data)
}
}
我们看下输出
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
Running `target\debug\rustDemo01.exe`
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
我们发现刚好与输入的顺序相反,是后进先出,这里可以结合之前的生命周期就明白为啥了,我能保持和self一样长的寿命,但是self是后出,所以我比self先出,self都没了,我的寿命也早就没了
使用std::mem::drop来提前drop值
drop
功能,也没必要,因为Drop trait
的目的就是进行自动的释放处理逻辑Drop trait
的drop
方法std::mem::drop
函数,来提前drop
值drop
即使写了多次也不会出现double free
的情况我们在上面的例子中试下:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.29s
Running `target\debug\rustDemo01.exe`
Dropping CustomSmartPointer with data `my stuff`!
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
有时一个值会有多个所有者,例如下面的6
就有多个变量同时引用它
为了支持多重所有权可以使用Rc
(reference couting引用计数),即可以追踪所有到值的引用,如果是0
个引用,该值可能被清理掉
Rc
的使用场景 :
- 需要在heap上分配数据,这些数据被程序的多个部分读取(只读),但在编译时无法确定哪个部分最后使用完这些数据
- Rc
只适合单线程的场景(14章会研究多线程的场景)
我们用一个例子来使用一下
我们看下面的代码,其实会报错
这个时候我们就可以使用Rc
这个数据类型了
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5,
Rc::new(Cons(10,
Rc::new(Nil) ))));
// 这里可以通过clone获得引用,而不是所有权
let b = Cons(3, Rc::clone(&a)); // 计数器加一
let c = Cons(3, Rc::clone(&a)); // 计数器加一
}
enum List {
Cons(i32, Rc<List>),
Nil
}
使用Rc::clone(&a)
可以获得不可变的引用,并且引用计数器会加一,等该引用结束(离开其作用域后)计数器将减一
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5,
Rc::new(Cons(10,
Rc::new(Nil) ))));
println!("count after creating a = {}", Rc::strong_count(&a)); // 1
// 这里可以通过clone获得引用,而不是所有权
let b = Cons(3, Rc::clone(&a)); // 计数器加一
println!("count after creating b = {}", Rc::strong_count(&a)); // 2
{
let c = Cons(3, Rc::clone(&a)); // 计数器加一
println!("count after creating c = {}", Rc::strong_count(&a)); // 3
}
println!("count after c goes out of scope = {}", Rc::strong_count(&a)); // 2
}
enum List {
Cons(i32, Rc<List>),
Nil
}
Rc::clone()
与 类型的clone()
方法
Rc::clone()
:增加引用,不会执行数据的深度拷贝操作- 类型的clone方法:很多都会深拷贝
Rc
通过不可变引用,使你可以在程序不同部分之间共享只读数据
Rc
为了防止悬垂引用,不允许&
多引用。而Rc里维护了一个引用计数器,会在没有引用时析构。防止一段内存多次free
内部可变性(interior mutability)
unsafe
代码来绕过Rust正常的可变性和借用规则RefCell
Rc
相似,只能用于单线程
场景这里我们回忆一下借用规则:
- 在任何给定的时间里,你要么只能拥有一个可变引用,要么只能拥有任意数量的不可变引用
- 引用总是有效的
内部可变性:允许可变的借用一个不可变的值 (rust所有权规则是不允许的)
如果没有内部可变性,下面的代码就无法修改
我们来看一下详细一点的例子
/// 这里简单说就是有一个trait规定了只能是对结构体的不可变引用
/// 但我们希望实现这个trait的时候能对结构体进行修改
/// 就可以把希望修改的字段用智能指针包装一下
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: 'a + Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl <'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
LimitTracker { messenger, value: 0, max }
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger.send("Urgent warning: You've used up over 90% of your");
} else if percentage_of_max >= 0.75 {
self.messenger.send("Warning: You've used up over 75% of your quota");
}
}
}
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger { sent_messages: vec![] }
}
}
impl Messenger for MockMessenger {
// 2. 确实需要改变实参的状态(需要的是可变引用),但又不能修改接口定义
// 3. 这时就可以用不安全的refcell方法,传入不可变引用,以改变值
fn send(&mut self, msg: &str) {
self.sent_messages.push(String::from(msg));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messager = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messager, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messager.sent_messages.len(), 1);
}
}
使用RefCell
修改后逻辑如下:
/// 这里简单说就是有一个trait规定了只能是对结构体的不可变引用
/// 但我们希望实现这个trait的时候能对结构体进行修改
/// 就可以把希望修改的字段用智能指针包装一下
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: 'a + Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl <'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &T, max: usize) -> LimitTracker<T> {
LimitTracker { messenger, value: 0, max }
}
pub fn set_value(&mut self, value: usize) {
self.value = value;
let percentage_of_max = self.value as f64 / self.max as f64;
if percentage_of_max >= 1.0 {
self.messenger.send("Error: You are over your quota!");
} else if percentage_of_max >= 0.9 {
self.messenger.send("Urgent warning: You've used up over 90% of your");
} else if percentage_of_max >= 0.75 {
self.messenger.send("Warning: You've used up over 75% of your quota");
}
}
}
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger { sent_messages: RefCell::new(vec![])}
}
}
impl Messenger for MockMessenger {
// 2. 确实需要改变实参的状态(需要的是可变引用),但又不能修改接口定义
// 3. 这时就可以用不安全的refcell方法,传入不可变引用,以改变值
fn send(&self, msg: &str) {
self.sent_messages.borrow_mut().push(String::from(msg));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
let mock_messager = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messager, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messager.sent_messages.borrow().len(), 1);
}
}
我们来看下修改的内容:
上面的修改中,我们将不可变的引用套在
RefCell
中
我们直接看例子
use crate::List::{Cons, Nil};
use std::{rc::Rc, cell::RefCell};
fn main() {
let value = Rc::new(RefCell::new(5));
let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil)));
let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil
}
输出
$ cargo run
Compiling refdemo v0.1.0 (E:\workspace\vscode\rustStudy\refdemo)
Finished dev [unoptimized + debuginfo] target(s) in 0.36s
Running `target\debug\refdemo.exe`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))
其他可实现内部可变性的类型
当我们使用Rc
和Rcell
就可能创造出循环引用,从而发生内存泄漏
那么如何防止内存泄漏呢
我们来看个例子
use std::{rc::Rc, cell::RefCell};
use crate::List::{Cons, Nil};
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
println!("a initial rc count = {}", Rc::strong_count(&a));
println!("a next item = {:?}", a.tail());
let b = Rc::new(Cons(10, RefCell::new(Rc::clone(&a))));
println!("a rc count after b creation = {}", Rc::strong_count(&a));
println!("b initial rc count = {}", Rc::strong_count(&b));
println!("b next item = {:?}", b.tail());
if let Some(link) = a.tail() {
*link.borrow_mut() = Rc::clone(&b);
}
println!("b rc count after changing a = {}", Rc::strong_count(&b));
println!("a rc count after changing a = {}", Rc::strong_count(&a));
// cycle, it will overflow the stack
println!("a next item = {:?}", a.tail())
}
#[derive(Debug)]
enum List {
Cons(i32, RefCell<Rc<List>>),
Nil
}
impl List {
fn tail(&self) -> Option<&RefCell<Rc<List>>> {
match self {
Cons(_, item) => Some(item),
Nil => None,
}
}
}
我们允许一下
$ cargo run
Compiling demo03 v0.1.0 (E:\workspace\vscode\rustStudy\demo03)
Finished dev [unoptimized + debuginfo] target(s) in 0.23s
Running `target\debug\demo03.exe`
a initial rc count = 1
a next item = Some(RefCell { value: Nil })
a rc count after b creation = 2
b initial rc count = 1
b next item = Some(RefCell { value: Cons(5, RefCell { value: Nil }) })
b rc count after changing a = 2
a rc count after changing a = 2
a next item = Some(RefCell { value: Cons(10, RefCell ...省略很多
thread 'main' has overflowed its stack
error: process didn't exit successfully: `target\debug\demo03.exe` (exit code: 0xc00000fd, STATUS_STACK_OVERFLOW)
为了防止循环引用可以将
Rc
换成Weak
Rc::clone
为Rc
实例的strong_count
加1,Rc
的实例只有在strong_count
为0的时候才会被清理Rc
实例通过调用Rc::downgrade
方法可以创建值的Weak Reference (弱引用)
- 返回的类型是
Weak
(智能指针) - 调用
Rc::downgrade会为weak_count
加1Rc
使用weak_count
来追踪存在多少Weak
weak_count
不为0并不影响Rc
实例的清理
在本章介绍了标准库中常见的智能指针
Box
:在heap内存上分配值Rc
:启用多重所有权的引用计数类型Ref
和RefMut
,通过RefCell
访问:在运行时而不是编译时强制借用规则的类型此外:
在大部分OS里,代码允许在进程(process)
中,OS同时管理多个进程。在我们的程序中,各独立部分可以同时运行,运行这些独立部分的就是线程Thread
多线程运行一般:
多线程会带来一些问题:
在常见的实现线程的方式有:
1:1模型
,需要比较小的运行时M:N模型
,需要更大的运行时rust为了权衡运行时的支持,标准库仅提供
1:1
模型的线程
在rust中可以通过spawn
创建新线程
use std::{thread, time::Duration};
fn main() {
thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
}
我们运行一下
$ cargo run
Compiling threadStudy v0.1.0 (E:\workspace\vscode\rustStudy\threadStudy)
Finished dev [unoptimized + debuginfo] target(s) in 0.47s
Running `target\debug\threadStudy.exe`
hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
当然也可以join
一下,通过join Handle
来等待所有线程的完成
thread::spawn
函数的返回值类型是 JoinHandleuse std::{thread, time::Duration};
fn main() {
let handle = thread::spawn(|| {
for i in 1..10 {
println!("hi number {} from the spawned thread!", i);
thread::sleep(Duration::from_millis(1));
}
});
for i in 1..5 {
println!("hi number {} from the main thread!", i);
thread::sleep(Duration::from_millis(1));
}
handle.join().unwrap();
}
$ cargo run
Compiling threadStudy v0.1.0 (E:\workspace\vscode\rustStudy\threadStudy)
Finished dev [unoptimized + debuginfo] target(s) in 0.47s
Running `target\debug\threadStudy.exe
hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the spawned thread!
hi number 2 from the main thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!
hi number 6 from the spawned thread!
hi number 7 from the spawned thread!
hi number 8 from the spawned thread!
hi number 9 from the spawned thread!
使用move闭包
thread::spawn
函数一起使用,它允许你使用其他线程的数据我们看下面的代码
mod tests {
use std::thread;
#[test]
fn test01() {
let v = vec![1, 2, 3];
let handle = thread::spawn(|| {
println!("Here's a vector:{:?}", v);
});
handle.join().unwrap();
}
}
如果允许会报错,因为在闭包里使用的v
,不确定什么时候会被回收
error[E0373]: closure may outlive the current function, but it borrows `v`, which is owned by the current function
--> src\main.rs:25:36
|
25 | let handle = thread::spawn(|| {
| ^^ may outlive borrowed value `v`
26 | println!("Here's a vector:{:?}", v);
| - `v` is borrowed here
|
note: function requires argument type to outlive `'static`
--> src\main.rs:25:22
|
25 | let handle = thread::spawn(|| {
| ______________________^
26 | | println!("Here's a vector:{:?}", v);
27 | | });
| |__________^
help: to force the closure to take ownership of `v` (and any other referenced variables), use the `move` keyword
|
25 | let handle = thread::spawn(move || {
| ++++
所以我们可以使用move
关键字将v
的所有权移入到闭包里面
mod tests {
use std::thread;
#[test]
fn test01() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Here's a vector:{:?}", v);
});
handle.join().unwrap();
}
}
线程之间通信的方式有很多种,例如Java中使用共享内存的方式进行通信,比如常见的ReentrantLock
里面为了记录锁重入次数,使用的volatile
关键字修饰的共享内存:state
现在有一种很流行且能保证安全并发的技术是:消息传递
线程(或Actor)通过彼此发送消息(数据)来进行通信
GO的名言就是:不要用共享内存来通信,要用通信来共享内存
创建channel可以使用mpsc::channel
函数来创建channel
举个例子
#[test]
fn test02() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hello");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {}", received); // Got: hello
}
发送端的
send
方法
- 参数:想要发送的数据
- 返回:Result
,如果有问题(例如接收端已经被丢弃),就返回一个错误 接收端的
recv
方法
recv
方法:阻塞当前线程执行,直到channel中有值被送来,收到后返回Result
,如有问题返回错误try_recv
方法:不会阻塞- 立刻返回Result
- 有数据到达,返回OK,里面包裹着数据
- 否则,返回错误
- 通常会使用循环调用来检查
try_recv
的结果
所有权在消息系统中传递时非常重要的,可以帮助我们编写安全、并发的代码
上面的例子中如果我们想要把已经发送到channel里面的值再打印一下的话,就会报错
可以不断的向channel中发送元素,然后进行接收
#[test]
fn test04() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let vals = vec![
String::from("hello"),
String::from("hello"),
String::from("hello"),
String::from("hello"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_millis(100));
}
});
for received in rx {
println!("Got: {}", received)
}
}
我们可以使用clone来创建多个生产者
#[test]
fn test05() {
let (tx, rx) = mpsc::channel();
let txcopy = mpsc::Sender::clone(&tx);
thread::spawn(move || {
let vals = vec![
String::from("sendCopy: hello"),
String::from("sendCopy: hello"),
String::from("sendCopy: hello"),
String::from("sendCopy: hello"),
];
for val in vals {
txcopy.send(val).unwrap();
thread::sleep(Duration::from_millis(100));
}
});
thread::spawn(move || {
let vals = vec![
String::from("send: hello"),
String::from("send: hello"),
String::from("send: hello"),
String::from("send: hello"),
];
for val in vals {
tx.send(val).unwrap();
thread::sleep(Duration::from_millis(100));
}
});
for received in rx {
println!("Got: {}", received)
}
}
// 输出
Got: sendCopy: hello
Got: send: hello
Got: send: hello
Got: sendCopy: hello
Got: send: hello
Got: sendCopy: hello
Got: send: hello
Got: sendCopy: hello
上一节讲的是使用channel的方法进行并发,这里要使用共享内存
的方式进行并发
channel类似单所有权,一旦将值的所有权转移至channel
,就无法使用它了
共享内存并发类似多所有权:多个线程可以同时访问同一块内存
既然有共享内存,那么保障线程安全的方式最简单的就是加锁,rust提供了Mutex
Mutex是mutual exclusion(互斥锁)的简写
Mutex的两条规则
- 在使用数据之前,必须尝试获取锁(lock)
- 使用完mutex所保护的数据,必须对数据进行解锁,以便其他线程可以获取锁
Mutex
举个例子
#[test]
fn test06() {
let m = Mutex::new(5);
{
let mut num = m.lock().unwrap();
*num = 6;
// mutex 实现了drop trait,所以作用域完之后会自动解锁
}
}
我们再看一段代码
这里因为counter
在第一次循环的时候所有权已经被移动过了,所以后面的线程获取不到所有权
这里我们要用到多线程的多重所有权这个概念了
上面的例子我们可以使用Arc
来进行原子引用计数,跟Rc不同的是可以用于并发场景
#[test]
fn test07() {
let counter = Arc::new( Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap()); // 10
}
RefCell
/ Rc VS Mutex / Arc
- Mutex
提供了内部可变性,和Cell家族一样 - 我们使用RecCell
来改变Rc 里面的内容 - 我们使用Mutex
来改变Arc 里面的内容 - 注意:Mutex
有死锁的风险
rust语言的并发特性是比较少的,目前的并发都是来自标准库的(而不是语言本身)
我们无需局限于标准库的并发 ,可以自己实现并发
在rust中有两个并发的概念:
std::marker:Sync
和std::marker::Send
这两个traitSend是一个Trait
Send trait
:允许线程间转移所有权,Rust里几乎所有的类型都实现了Send(Rc没有实现,只能用于单线程的场景) - 任何完全由Send类型组成的类型也被标记为Send
- 除了原始指针之外,几乎所有的基础类型都是Send
Sync:允许从多线程访问
- 实现Sync的类型可以安全的被多个线程引用
- 也就是说:如果T是Sync,那么&T也是Send(引用可以被安全的送往另一个线程)
- 基础类型都是Sync
- 完全有Sync类型组成的类型也是Sync
- 但是,Rc
不是Sync的 - RefCell
和Cell 家族也不是Sync的 - Mutex
是Sync的 最后注意:如果我们自己手动实现Send和Sync是及其不安全的,我们很难保证线程安全!
rust在设计的时候收到很多编程范式的影响,包括面向对象,面向对象通常包含以下特征:封装、继承、多态
《设计模式》作者GOF四人帮中给面向对象的定义:
基于此定义,Rust是面向对象的
我们对面向对象的三个特征进行分析:
pub
关键字,并且struct里面的结构体默认就是私有的,所以rust是满足封装的特性的trait
来进行代码分享我们现在有一个需求,创建一个GUI工具:
在面向对象的语言中,做法通常是这样的:
在Rust中是这样做的,为公有行为定义一个trait:
可以用个小例子来理解一下:
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
// 只要实现了Draw就可以放到里面
pub components: Vec<Box<dyn Draw>>
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
// 绘制一个按钮
println!("Button draw")
}
}
// --------------------------------------------------------
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// 绘制一个选择框
println!("SelectBox draw")
}
}
mod tests {
use crate::{Screen, SelectBox, Button};
#[test]
fn test01() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
]
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
})
]
};
screen.run(); // SelectBox draw \n Button draw
}
}
Trait对象执行的是动态派发
- 将trait约束作用于泛型时,Rust编译器会执行单态化
- 编译器会为我们用来替换泛型类型参数的每一个具体类型生成对应函数和方法的非泛型实现
- 通过单态化生成的代码会执行
静态派发(static dispatch)
,在编译过程中确定调用的具体方法- 动态派发
- 无法在编译过程中确定你调用的究竟是哪一种方法
- 编译器会产生额外的代码以便于在运行时找出希望调用的方法
我们使用trait对象,会执行动态派发:
- 产生运行时开销
- 阻止编译器内联方法代码,使得部分优化操作无法进行
Trait对象必须保证对象安全
- 只能把满足对象安全(object-safe)的trait转化为trait对象
- rust采用一系列规则来判定某个对象是否安全,我们只需要记住两条
- 方法的返回类型不是
Self
(如果返回Self,则指实现了该Trait的实例,大小是不确定的)- 方法中不包含任何泛型类型参数(泛型同理)
接下来我们用rust实现一些常见的设计模式
模式是Rust中的一种特殊语法,用于匹配复杂和简单类型的结构
将模式与匹配表达式和其他构造结合使用,可以更好的控制程序的控制流
模式由以下元素(的一些组合)组成:
想要使用模式,需要将其与某个值进行比较,如果模式匹配,就可以在代码中使用这个值的相应部分
模式出现在 Rust 的很多地方。你已经在不经意间使用了很多模式!本部分是一个所有有效模式位置的参考
for
循环是 Rust 中最常见的循环结构,不过还没有讲到的是 for
可以获取一个模式。在 for
循环中,模式是 for
关键字直接跟随的值,正如 for x in y
中的 `x这里我们主要了解模式是否会无法匹配
。模式分为两种:
None
就会匹配失败,也就是可辨驳的,可失败的函数参数、
let
语句、for
循环只接受无可辩驳的模式(也就是不能匹配失败)
if let
和while let
接受可辨驳和无可辩驳的模式
#![allow(unused_variables)]
fn main() {
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
3 => println!("three"),
_ => println!("anything"),
}
}
命名变量是匹配任何值的不可反驳模式,这在之前已经使用过数次。然而当其用于 match
表达式时情况会有些复杂。因为 match
会开始一个新作用域,match
表达式中作为模式的一部分声明的变量会覆盖 match
结构之外的同名变量,与所有变量一样。在示例 18-11 中,声明了一个值为 Some(5)
的变量 x
和一个值为 10
的变量 y
。接着在值 x
上创建了一个 match
表达式。观察匹配分支中的模式和结尾的 println!
,并在运行此代码或进一步阅读之前推断这段代码会打印什么
fn main() {
let x = Some(5);
let y = 10;
match x {
Some(50) => println!("Got 50"),
// 这里的y并不是外面的变量,而是一个命名变量,所以输出5
Some(y) => println!("Matched, y = {:?}", y),
_ => println!("Default case, x = {:?}", x),
}
// 此作用域下的y是变量y
println!("at the end: x = {:?}, y = {:?}", x, y);
}
最后的输出是
Matched, y = 5
at the end: x = Some(5), y = 10
在match表达式中,使用|
语法(就是或的意思),可以匹配多种模式
#![allow(unused_variables)]
fn main() {
let x = 1;
match x {
1 | 2 => println!("one or two"),
3 => println!("three"),
_ => println!("anything"),
}
}
通过 ..=
匹配值的范围
..=
语法允许你匹配一个闭区间范围内的值。在如下代码中,当模式匹配任何在此范围内的值时,该分支会执行:
#![allow(unused_variables)]
fn main() {
let x = 5;
match x {
1..=5 => println!("one through five"),
_ => println!("something else"),
}
}
范围只允许用于数字或 char
值,因为编译器会在编译时检查范围不为空。char
和 数字值是 Rust 仅有的可以判断范围是否为空的类型。
#![allow(unused_variables)]
fn main() {
let x = 'c';
match x {
'a'..='j' => println!("early ASCII letter"),
'k'..='z' => println!("late ASCII letter"),
_ => println!("something else"),
}
}
// Rust 知道 c 位于第一个模式的范围内,并会打印出 early ASCII letter。
解构结构体
#[test]
fn test03() {
struct Point {
x: u32,
y: u32,
}
let p = Point { x: 0, y: 7 };
// 解构结构体,这样相当于对变量a、b进行赋值
let Point { x: a, y: b } = p;
assert_eq!(0, a);
assert_eq!(7, b);
// 上面的写法有些麻烦,可以简写为
let Point { x, y} = p;
assert_eq!(0, x);
assert_eq!(7, y);
// 也可以更加灵活的使用
match p {
Point {x, y: 0} => println!("On the x axis at {}", x),
Point {x:0, y} => println!("On the x axis at {}", y),
Point {x, y} => println!("On neither axis:({}, {})", x, y),
}
}
结构枚举
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
fn main() {
let msg = Message::ChangeColor(0, 160, 255);
match msg {
Message::Quit => {
println!("The Quit variant has no data to destructure.")
}
Message::Move { x, y } => {
println!(
"Move in the x direction {} and in the y direction {}",
x,
y
);
}
Message::Write(text) => println!("Text message: {}", text),
Message::ChangeColor(r, g, b) => {
println!(
"Change the color to red {}, green {}, and blue {}",
r,
g,
b
)
}
}
}
解构嵌套的结构体和枚举
enum Color {
Rgb(i32, i32, i32),
Hsv(i32, i32, i32),
}
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(Color),
}
fn main() {
let msg = Message::ChangeColor(Color::Hsv(0, 160, 255));
match msg {
Message::ChangeColor(Color::Rgb(r, g, b)) => {
println!(
"Change the color to red {}, green {}, and blue {}",
r,
g,
b
)
}
Message::ChangeColor(Color::Hsv(h, s, v)) => {
println!(
"Change the color to hue {}, saturation {}, and value {}",
h,
s,
v
)
}
_ => ()
}
}
解构结构体和元组
甚至可以用复杂的方式来混合、匹配和嵌套解构模式。如下是一个复杂结构体的例子,其中结构体和元组嵌套在元组中,并将所有的原始类型解构出来
#![allow(unused_variables)]
fn main() {
struct Point {
x: i32,
y: i32,
}
let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });
}
忽略模式中的值
有时忽略模式中的一些值是有用的,比如 match
中最后捕获全部情况的分支实际上没有做任何事,但是它确实对所有剩余情况负责。有一些简单的方法可以忽略模式中全部或部分值:使用 _
模式(我们已经见过了),在另一个模式中使用 _
模式,使用一个以下划线开始的名称,或者使用 ..
忽略所剩部分的值。让我们来分别探索如何以及为什么要这么做
使用 _ 忽略整个值
我们已经使用过下划线(_
)作为匹配但不绑定任何值的通配符模式了。虽然 _
模式作为 match
表达式最后的分支特别有用,也可以将其用于任意模式,包括函数参数中
fn foo(_: i32, y: i32) {
println!("This code only uses the y parameter: {}", y);
}
fn main() {
foo(3, 4);
}
使用嵌套的 _ 忽略部分值
#![allow(unused_variables)]
fn main() {
let mut setting_value = Some(5);
let new_setting_value = Some(10);
match (setting_value, new_setting_value) {
// 只要是Some类型就行,里面是啥值不在乎
(Some(_), Some(_)) => {
println!("Can't overwrite an existing customized value");
}
_ => {
setting_value = new_setting_value;
}
}
println!("setting is {:?}", setting_value);
}
还可以匹配想要的数据
#[test]
fn test04() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers:{}, {}, {}", first, third, fifth)
}
}
}
通过在名字前以一个下划线开头来忽略未使用的变量
解构也会转移所有权
用 … 忽略剩余值
#![allow(unused_variables)]
fn main() {
struct Point {
x: i32,
y: i32,
z: i32,
}
let origin = Point { x: 0, y: 0, z: 0 };
match origin {
Point { x, .. } => println!("x is {}", x),
}
// 还可以这样
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, .., fifth) => {
println!("Some numbers:{}, {}, {}", first, third, fifth)
}
}
}
匹配守卫提供的额外条件
匹配守卫(match guard)是一个指定于 match
分支模式之后的额外 if
条件,它也必须被满足才能选择此分支。匹配守卫用于表达比单独的模式所能允许的更为复杂的情况。
这个条件可以使用模式中创建的变量。下面 展示了一个 match
,其中第一个分支有模式 Some(x)
还有匹配守卫 if x < 5
#![allow(unused_variables)]
fn main() {
let num = Some(4);
match num {
Some(x) if x < 5 => println!("less than five: {}", x),
Some(x) => println!("{}", x),
None => (),
}
}
#![allow(unused_variables)]
fn main() {
let x = 4;
let y = false;
match x {
4 | 5 | 6 if y => println!("yes"),
_ => println!("no"),
}
}
@ 绑定
at 运算符(@
)允许我们在创建一个存放值的变量的同时测试其值是否匹配模式
#![allow(unused_variables)]
fn main() {
enum Message {
Hello { id: i32 },
}
let msg = Message::Hello { id: 5 };
match msg {
Message::Hello { id: id_variable @ 3..=7 } => {
println!("Found an id in range: {}", id_variable)
},
Message::Hello { id: 10..=12 } => {
println!("Found an id in another range")
},
Message::Hello { id } => {
println!("Found some other id: {}", id)
},
}
}
这节包含的内容有:
许多语言里面都有unsafe,在rust中隐藏了太多的内存安全保障
但是在rust中还隐藏这Unsafe rust
,它没有强制内存安全保证,但是提供了许多黑魔法
unsafe rust存在的原因有:
unsafe超能力
使用unsafe关键字来切换到unsafe rust,可以开启一个代码块,里面存放unsafe的代码
unsafe rust里可以执行四个动作(unsafe超能力)
unsafe trait
注意:
- unsafe并没有关闭借用检查或停用其他安全检查
- 任何内存安全相关的错误必须留在unsafe块中
- 尽可能隔离unsafe代码,最好将其封装在安全的抽象里,提供安全的API
原始指针
*mut T
*const T
。意味着指针在解引用后不能直接对其进行赋值*
不是解引用符号,它是类型名的一部分与引用不同,原始指针:
放弃保证的安全,换取更好的性能/与其他语言或硬件接口的能力
我们来看一个原始指针的例子
#[test]
fn test05() {
let mut num = 5;
let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;
// 不会报错
unsafe {
println!("r1: {}", *r1);
println!("r2: {}", *r2);
}
// 声明了一个未知的内存地址,取不到想要的i32类型,所以会报错
let address = 0x012345usize;
let r = address as *const i32;
unsafe {
println!("r: {}", *r);
}
}
那么我们为什么要使用原始指针呢?
调用unsafe函数或方法
创建unsafe代码的安全抽象
函数包含unsafe代码并不意味着需要将整个函数标记为unsafe
将unsafe代码包裹在安全函数中是一个常见的抽象
使用extern函数调用外部代码
有时你的 Rust 代码可能需要与其他语言编写的代码交互。为此 Rust 有一个关键字,extern
,有助于创建和使用 外部函数接口(Foreign Function Interface, FFI)。外部函数接口是一个编程语言用以定义函数的方式,其允许不同(外部)编程语言调用这些函数。
下面展示了如何集成 C 标准库中的 abs
函数。extern
块中声明的函数在 Rust 代码中总是不安全的。因为其他语言不会强制执行 Rust 的规则且 Rust 无法检查它们,所以确保其安全是程序员的责任:
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
在 extern "C"
块中,列出了我们希望能够调用的另一个语言中的外部函数的签名和名称。"C"
部分定义了外部函数所使用的 应用程序接口(application binary interface,ABI) —— ABI 定义了如何在汇编语言层面调用此函数。"C"
ABI 是最常见的,并遵循 C 编程语言的 ABI
从其他语言调用rust函数
#[no_mangle]
注解,避免rust在编译时改变它的名称在如下的例子中,一旦其编译为动态库并从 C 语言中链接,call_from_c
函数就能够在 C 代码中访问:
fn main() {
#[no_mangle]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
}
extern
的使用无需 unsafe
访问或修改一个可变静态变量
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("name is: {}", HELLO_WORLD);
}
静态变量
const MAX_NUM: u32 = 100
'static
生命周期的引用,无需显示标注static mut COUNTER: u32 = 0;
fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
add_to_count(3);
unsafe {
println!("COUNTER: {}", COUNTER); // COUNTER: 3
}
}
实现不安全(unsafe)trait
#![allow(unused_variables)]
fn main() {
unsafe trait Foo {
// methods go here
}
unsafe impl Foo for i32 {
// method implementations go here
}
}
何时使用不安全代码
使用 unsafe
来进行这四个操作(超级力量)之一是没有问题的,甚至是不需要深思熟虑的,不过使得 unsafe
代码正确也实属不易,因为编译器不能帮助保证内存安全。当有理由使用 unsafe
代码时,是可以这么做的,通过使用显式的 unsafe
标注使得在出现错误时易于追踪问题的源头
在trait定义中使用关联类型来指定占位类型
举个例子
pub trait Iterator {
type Item
fn next(&mut self) -> Option<Self::Item>;
}
fn main() {
println!("hello world")
}
看起来我们使用泛型也可以达到同样的效果,但是如果我们使用泛型的话就不得不在每一个实现中标注类型
fn main() {
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
}
当 trait 有泛型参数时,可以多次实现这个 trait,每次需改变泛型参数的具体类型。接着当使用 Counter
的 next
方法时,必须提供类型注解来表明希望使用 Iterator
的哪一个实现。
通过关联类型,则无需标注类型因为不能多次实现这个 trait。对于上面例子中使用关联类型的定义,我们只能选择一次 Item
会是什么类型,因为只能有一个 impl Iterator for Counter
。当调用 Counter
的 next
时不必每次指定我们需要 u32
值的迭代器
默认泛型参数和运算符重载
std::ops
中列出的那些trait来重载一部分相应的运算符举个例子
use std::ops::Add;
#[derive(Debug, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
// 这里实现了运算符重载
assert_eq!(Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 });
}
上面的例子中实现 Add
trait 重载 Point
实例的 +
运算符
add
方法将两个 Point
实例的 x
值和 y
值分别相加来创建一个新的 Point
。Add
trait 有一个叫做 Output
的关联类型,它用来决定 add
方法的返回值类型。
这里默认泛型类型位于 Add
trait 中。这里是其定义:
fn main() {
trait Add<RHS=Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
}
这看来应该很熟悉,这是一个带有一个方法和一个关联类型的 trait。比较陌生的部分是尖括号中的 RHS=Self
:这个语法叫做 默认类型参数(default type parameters)。RHS
是一个泛型类型参数(“right hand side” 的缩写),它用于定义 add
方法中的 rhs
参数。如果实现 Add
trait 时不指定 RHS
的具体类型,RHS
的类型将是默认的 Self
类型,也就是在其上实现 Add
的类型。
当为 Point
实现 Add
时,使用了默认的 RHS
,因为我们希望将两个 Point
实例相加。让我们看看一个实现 Add
trait 时希望自定义 RHS
类型而不是使用默认类型的例子
这里有两个存放不同单元值的结构体,Millimeters
和 Meters
。我们希望能够将毫米值与米值相加,并让 Add
的实现正确处理转换。可以为 Millimeters
实现 Add
并以 Meters
作为 RHS
举个例子
fn main() {
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
}
为了使 Millimeters
和 Meters
能够相加,我们指定 impl Add
来设定 RHS
类型参数的值而不是使用默认的 Self
。
默认参数类型主要用于如下两个方面:
标准库的 Add
trait 就是一个第二个目的例子:大部分时候你会将两个相似的类型相加,不过它提供了自定义额外行为的能力。在 Add
trait 定义中使用默认类型参数意味着大部分时候无需指定额外的参数。换句话说,一小部分实现的样板代码是不必要的,这样使用 trait 就更容易了。
第一个目的是相似的,但过程是反过来的:如果需要为现有 trait 增加类型参数,为其提供一个默认类型将允许我们在不破坏现有实现代码的基础上扩展 trait 的功能。
完全限定语法(Fully Qualified Syntax)与消歧义:调用相同名称的方法
我们先看下下面的代码,看会输出什么
fn main() {
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
let person = Human;
person.fly(); // *waving arms furiously*
}
那么怎么调用对应trait里面的方法呢?
Pilot::fly(&person);
Wizard::fly(&person);
但是如果我们在定义trait
的时候没有把self
那该怎么办呢?这个时候就可以使用完全限定语法了
完全限定语法:
举个例子
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
// 使用完全限定语法
println!("A baby dog is called a {}", <Dog as Animal>::baby_name());
}
使用supertrait来要求trait附带其他trait的功能
有时候我们需要在一个trait中使用其他trait的功能
supertrait
举个例子
use std::fmt;
fn main() {
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
}
newtype 模式用以在外部类型上实现外部 trait
newtype
模式来绕过这一规则
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![String::from("hello"), String::from("world")]);
println!("w = {}", w); // w = [hello, world]
}
使用newtype
模式实现类型安全和抽象,我们先总结一下newtype
模式的作用:
使用类型别名创建类型的同义词
rust提供了类型别名的功能(主要用途是为了减少代码字符重复):
type
关键字fn main() {
type Kilometers = i32;
let x: i32 = 5;
let y: Kilometers = 5;
println!("x + y = {}", x + y);
}
Never类型
有一个名为!
的特殊类型,其实是rust为了类型一致性引入的概念
never
类型,因为它在不返回的函数中充当返回类型不返回值的函数也被称作发散函数(diverging function)
动态大小和Sized Trait
str
,就是只能在运行时才能确定字符串的长度(动态空间)Rust 需要知道应该为特定类型的值分配多少内存,同时所有同一类型的值必须使用相同数量的内存。如果允许编写这样的代码,也就意味着这两个 str
需要占用完全相同大小的空间,不过它们有着不同的长度。这也就是为什么不可能创建 一个存放动态大小类型的变量的原因
Rust使用动态大小类型的通用方式
通过&str
我们可以知道rust在存储动态大小类型时候的做法为:
另外一种动态大小的类型:trait
&dyn Trait
或Box(Rc)
之后Sized trait
为了处理动态大小得类型,Rust提供了一个Sized trait
来确定一个类型的大小在编译时是否已知
对于如下泛型函数定义:
fn generic<T>(t: T) {
// --snip--
}
实际上被当作如下处理:
fn generic<T: Sized>(t: T) {
// --snip--
}
泛型函数默认只能用于在编译时已知大小的类型。然而可以使用如下特殊语法来放宽这个限制
fn generic<T: ?Sized>(t: &T) {
// --snip--
}
?Sized
trait bound 与 Sized
相对;也就是说,它可以读作 “T
可能是也可能不是 Sized
的”。这个语法只能用于 Sized
,而不能用于其他 trait。
另外注意我们将 t
参数的类型从 T
变为了 &T
:因为其类型可能不是 Sized
的,所以需要将其置于某种指针之后。在这个例子中选择了引用
函数指针
fn
类型函数指针(function pointer)
fn add_one(x: i32) -> i32 {
x + 1
}
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
fn main() {
let answer = do_twice(add_one, 5);
println!("The answer is: {}", answer); // The answer is: 12
}
函数指针与闭包的不同
Fn trait
为约束的泛型参数trait
的泛型来编写函数:可以同时接受闭包和普通函数fn
而不接受闭包
#[test]
fn test07() {
let list_of_numbers = vec![1, 2, 3];
let list_of_strings:Vec<String> = list_of_numbers.iter()
.map(|i| i.to_string())
.collect();
// 可以写成这样
let list_of_numbers = vec![1, 2, 3];
let list_of_strings:Vec<String> = list_of_numbers.iter()
.map(ToString::to_string)
.collect();
}
返回闭包
闭包使用trait进行表达,无法在函数中直接返回一个闭包,可以将一个实现了该trait的具体类型作为返回值
宏 macro:宏在rust里指的是一组相关特性的集合称谓
macro_rules!
构建的声明宏(declarative macro)#[derive]宏
,用于struct
或enum
,可以为其指定随derive
属性添加的代码函数与宏的差别
macro_rules! 申明宏(可能马上要弃用了)
rust中最常见得宏形式:声明宏
macro_rules!
我们看看vec!
这个宏
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => {
{
let mut tmp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
基于属性来生成代码得过程宏
这种形式更像函数(某种形式得过程)一些
一共有三种过程宏
创建过程宏时:
自定义derive宏
现在有个需求,需要创建一个hello_macro
包,定义一个拥有关联函数hello_macro
的HelloMacro trait
#[derive(HelloMacro)]
,进而得到hello_macro
的默认实现类似属性的宏
属性宏与自定义derive宏类似
属性宏更加灵活
在这里我们会把之前学习到的知识点都穿起来
首先是main.rs
,这里需要添加一个包num_cpus = "1.0"
,用来获取CPU的核心数
use std::{
fs,
io::{Read, Write},
net::{TcpListener, TcpStream},
};
use httpServer::ThreadPool;
use num_cpus;
fn main() {
let listener = TcpListener::bind("127.0.0.1:8080").unwrap();
// 创建一个线程池
let pool = ThreadPool::new(num_cpus::get()); // 使用 num_cpus 库获取 CPU 核心数
for stream in listener.incoming() {
match stream {
Ok(stream) => {
// 使用多线程处理每个连接
pool.execute(|| {
handle_client(stream);
})
}
Err(e) => {
eprintln!("Error accepting connection: {}", e);
}
}
}
}
fn handle_client(mut stream: TcpStream) {
// 获取客户端的IP地址和端口号
let client_address = stream.peer_addr().unwrap();
println!("Client connected: {}", client_address);
// 读取客户端发送的数据
let mut buffer = [0; 1024];
stream.read(&mut buffer).unwrap();
// 解析请求数据,获取请求的 Host
let request = String::from_utf8_lossy(&buffer[..]);
let host = extract_host_from_request(&request);
// 打印请求的 Host 和 IP 信息
println!("========= 新的请求开始 ===========");
println!("Request Host: {}", host);
println!("Client IP: {}", client_address.ip());
// 构造响应
let response = read_file();
// 给客户端发送响应数据
let response_headers = format!(
"HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\n\r\n",
response.len()
);
let combined_response = format!("{}{}", response_headers, response);
stream.write(combined_response.as_bytes()).unwrap();
}
fn extract_host_from_request(request: &str) -> &str {
// 简化的从请求中提取 Host 的逻辑,实际情况可能更复杂
let host_start = request.find("Host: ").unwrap() + 6;
let host_end = request[host_start..].find("\r\n").unwrap() + host_start;
&request[host_start..host_end]
}
/// 缓存一下读取的文件 防止大量重复的磁盘IO
static mut RESPONSE: Option<String> = None;
fn read_file() -> &'static str {
unsafe {
if let Some(ref content) = RESPONSE {
return content;
}
let content = fs::read_to_string("hello.html").unwrap();
RESPONSE = Some(content.clone());
RESPONSE.as_ref().unwrap()
}
}
其次是lib.rs
,主要写的是自定义线程池的逻辑
use std::sync::{mpsc, Arc, Mutex};
use std::thread;
pub struct ThreadPool {
workers: Vec<Worker>,
/// thread: thread::JoinHandle<()> 是一个线程句柄,用于管理工作线程的生命周期。
/// JoinHandle 是一个类型,它可以在线程完成执行后进行线程的清理操作。
/// 这里的 <()> 表示线程执行完成后不返回任何结果
/// mpsc(多个生产者,单个消费者)通道,。每个工作线程都通过循环等待接收任务,然后执行任务
sender: mpsc::Sender<Job>,
}
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
assert!(size > 0);
let (sender, receiver) = mpsc::channel();
let receiver = Arc::new(Mutex::new(receiver));
let mut workers = Vec::with_capacity(size);
for id in 0..size {
workers.push(Worker::new(id, Arc::clone(&receiver)));
}
ThreadPool { workers, sender }
}
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
self.sender.send(job).unwrap();
}
}
struct Worker {
id: usize,
thread: thread::JoinHandle<()>,
}
type Job = Box<dyn FnOnce() + Send + 'static>;
impl Worker {
fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {
let thread = thread::spawn(move || loop {
let job = receiver.lock().unwrap().recv().unwrap();
println!("Worker {} got a job; executing.", id);
job();
});
Worker { id, thread }
}
}
压测一把
相比之下如果使用golang原生的http
库压测能到1w+的qps,主要是原生就已经支持io多路复用了
这里我们再使用actix-web
库起一个io多路复用的服务器压测试试
actix-web = "3.0"
use actix_web::{web, App, HttpServer, Responder};
async fn hello() -> impl Responder {
"Hello, World!"
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/hello", web::get().to(hello))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
最后压测结果也能达到1w的qps