RUST编程基础
编程概念
本节作为一个先驱章节,主要是对rust编程中一些基本概念的解释,或是rust语言中独有名词的解释。
rust编程概念
变量可变性
在rust中,变量默认是不可变的。
变量遮蔽
rust
//以下行为是被运行的
let x = 10;
let x = "hello"; //对上一个x进行了遮蔽,是一个全新的变量(重新内存分配)
语句和表达式
语句没有返回值。
表达式有返回值。注意表达式后面不能跟分号'';''。表达式如果不返回任何值,会隐式的返回一个单元类型()。
在rust中函数就是表达式
。
所有权
在rust中,任何内存对象都是有主人的,而且一般情况下完全属于它的主人,绑定就是把这个对象绑定给一个变量,让这个变量成为它的主人。
rust
//变量的绑定(赋值)
let a;
a = 10; //给变量a绑定内存对象(内存对象中的值为10)
所有权原则:
- Rust 中每一个值(这里的值可以理解为内存对象)都被一个变量所拥有,该变量被称为值的所有者
- 一个值同时只能被一个变量所拥有,或者说一个值只能拥有一个所有者
- 当所有者(变量)离开作用域范围时,这个值将被丢弃(drop)
所有权主要涉及堆区的浅复制问题,例如,
rust
let s1 = String::from("hello");
let s2 = s1; //s2获取了s1的所有权,s1就会失效,因为rust这里是采用浅复制,存储在堆区的hello的指针被赋予了s2,为了避免二次释放问题(s1、s2都会释放一次),rust认为s1在这里会马上失效。
在**栈区的变量(例如,内置类型)**就没有这个问题,可以参考c++的深复制和浅复制问题。
Rust 永远也不会自动创建数据的 "深拷贝" 。因此,任何自动的复制都不是深拷贝,如果我们确实需要深度复制 String
中堆上的数据,而不仅仅是栈上的数据,可以使用一个叫做 clone
的方法。如下:
rust
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
copy特征:如果一个类型有copy特征,一个旧的变量在被赋值给其他变量后仍然可用。赋值时根据Copy 性质来进行直接复制或做所有权转移(没有实现copy时)。
这里给出一个可以copy通用的规则,任何基本类型的组合可以 Copy ,不需要分配内存或某种形式资源的类型是可以 Copy 的,例如:
- 所有整数类型,比如u32
- 所有整数类型,比如u32
- 布尔类型bool,它的值是true 和false
- 所有浮点数类型,比如f64
- 字符类型char
- 元组,当且仅当其包含的类型也都是Copy的时候。比如,(i32, i32) 是Copy的,但(i32, String)就不是
- 不可变引用&T ,例如转移所有权中的最后一个例子,但是注意:
可变引用 &mut T是不可以 Copy的
生命周期
生命周期标注并不会改变任何引用的实际作用域,标记的生命周期只是为了取悦编译器,让编译器不要难为我们。生命周期标注方式,如下:
rust
//函数中标注生命周期,x、y 和返回值至少活得和 'a 一样久
fn useless<'a>(first: &'a i32, second: &'a i32)-> &'a str {}
//结构体中标注生命周期,字段part至少活的和结构体一样久
struct ImportantExcerpt<'a> {
part: &'a str,
}
//结构体方法中的生命周期标注规则,'a: 'b,是生命周期约束语法,跟泛型约束非常相似,用于说明 'a 必须比 'b 活得久
impl<'a: 'b, 'b> ImportantExcerpt<'a> {
fn announce_and_return_part(&'a self, announcement: &'b str) -> &'b str {
return announcement;
}
}
生命周期消除法则:
-
每一个引用参数都会获得独自的生命周期。
例如一个引用参数的函数就有一个生命周期标注:
fn foo<'a>(x: &'a i32)
,两个引用参数的有两个生命周期标注:fn foo<'a, 'b>(x: &'a i32, y: &'b i32)
, 依此类推。 -
若只有一个输入生命周期(函数参数中只有一个引用类型),那么该生命周期会被赋给所有的输出生命周期,也就是所有返回值的生命周期都等于该输入生命周期。
例如函数
fn foo(x: &i32) -> &i32
,x
参数的生命周期会被自动赋给返回值&i32
,因此该函数等同于fn foo<'a>(x: &'a i32) -> &'a i32
。 -
若存在多个输入生命周期,且其中一个是 &self 或 &mut self,则 &self 的生命周期被赋给所有的输出生命周期**。**
拥有
&self
形式的参数,说明该函数是一个方法
,该规则让方法的使用便利度大幅提升。
在 Rust 中有一个非常特殊的生命周期,那就是 'static
,拥有该生命周期的引用可以和整个程序活得一样久。字符串字面量,它是被硬编码进 Rust 的二进制文件中,因此这些字符串变量全部具有 'static
的生命周期。
rust
let s: &'static str = "我没啥优点,就是活得久,嘿嘿";
Self和self
在 Rust 中,有两个self
,一个指代当前的实例对象(self),一个指代特征或者方法类型的别名(Self)。
错误处理
1.panic
用于不可恢复错误时,当使用panic,程序会退出(注意,如果在非main线程中使用panic时,只会让该线程退出)。panic一般配合unwrap 和 expect使用,例如:
rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();//成功时返回OK(T),失败时直接panic
//let f = File::open("hello.txt").expect("Failed to open hello.txt");//expect能带上错误信息
}
2.Result
用于可恢复错误时,例如:
rust
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("Problem opening the file: {:?}", error)
},
};
}
3.符号?
rust
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;//打开成功,返回句柄给f,失败,函数直接返回
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
4.错误处理组合器
-
or()和and():
- or(),表达式按照顺序求值,若任何一个表达式的结果是 Some 或 Ok,则该值会立刻返回。
- and(),若两个表达式的结果都是 Some 或 Ok,则第二个表达式中的值被返回。若任何一个的结果是 None 或 Err ,则立刻返回。
-
or_else()和and_then(),与1类似,唯一的区别在于,它们的第二个表达式是一个闭包。
-
filter,对Option结果过滤:
rustfn main() { let s1 = Some(3); let s2 = Some(6); let n = None; let fn_is_even = |x: &i8| x % 2 == 0; assert_eq!(s1.filter(fn_is_even), n); // Some(3) -> 3 is not even -> None assert_eq!(s2.filter(fn_is_even), s2); // Some(6) -> 6 is even -> Some(6) assert_eq!(n.filter(fn_is_even), n); // None -> no value -> None }
-
map() 和 map_err(),map 可以将 Some 或 Ok 中的值映射为另一个;map_err可以将Err中的值映射成另一个。
-
map_or() 和 map_or_else(),map_or 在 map 的基础上提供了一个默认值;map_or_else 与 map_or 类似,但是它是通过一个闭包来提供默认值。
-
ok_or() and ok_or_else(),将 Option 类型转换为 Result 类型;ok_or 接收一个默认的 Err 参数;ok_or_else 接收一个闭包作为 Err 参数。
5.自定义错误类型
为了更好的定义错误,Rust 在标准库中提供了一些可复用的特征,例如 std::error::Error 特征:
rust
use std::fmt::{Debug, Display};
pub trait Error: Debug + Display {
fn source(&self) -> Option<&(Error + 'static)> { ... }
}
std::convert::From 特征可以将其它的错误类型转换成自定义的错误类型:
rust
pub trait From<T>: Sized {
fn from(_: T) -> Self;
}
6.将错误类型归一化的方式
- 使用特征对象 Box<dyn Error>。
- 自定义错误类型。
- 使用 thiserror/anyhow等三方库
注释
rust
//行注释
/*块注释*/
///文档注释
/**文档块注释*/
//!包、模块行注释,这些注释要添加到包、模块的最上方!
/*!包、模块行注释*/
变量声明顺序问题
fn do1(c: String) {}
:表示实参会将所有权传递给c
fn do2(c: &String) {}
:表示实参的不可变引用(指针)传递给c
,实参需带&
声明fn do3(c: &mut String) {}
:表示实参可变引用(指针)传递给c
,实参需带let mut
声明,且传入需带&mut
fn do4(mut c: String) {}
:表示实参会将所有权传递给c
,且在函数体内c
是可读可写的,实参无需mut
声明fn do5(mut c: &mut String) {}
:表示实参可变引用指向的值传递给c
,且c
在函数体内部是可读可写的,实参需带let mut
声明,且传入需带&mut
一句话总结:在函数参数中,冒号左边的部分,如:mut c
,这个 mut
是对🪄函数体内部有效🪄;冒号右边的部分,如:&mut String
,这个 &mut
是针对外部实参传入时的形式(声明)说明。
rust
fn main() {
let d1 = "str".to_string();
do1(d1);
let d2 = "str".to_string();
do2(&d2);
let mut d3 = "str".to_string();
do3(&mut d3);
let d4 = "str".to_string();
do4(d4);
let mut d5 = "str".to_string();
do5(&mut d5);
}
fn do1(c: String) {}
fn do2(c: &String) {}
fn do3(c: &mut String) {}
fn do4(mut c: String) {}
fn do5(mut c: &mut String) {}
//见圣经闭包章节评论
数据类型
rust是一门静态类型语言。
意味着编译器必须在编译期间就知道我们所有变量的类型。
变量/常量声明方式
rust
let [mut] var_name [:type] [=xxx];
const var_name <:type> = xxx; //值得类型必须标注
基本类型
整数类型
整数类型:
整型变量的字面表示:
整数溢出显示处理函数:
wrapping_xx、checked_xx、overflowing_xx、saturating_xx。
注意,在使用debug编译时,如果出现整数溢出,则编译会失败。
Nan类型:
Nan,对于数学上未定义的结果,rust浮点数使用Nan来表示。需要注意的是,Nan无法进行比较,但是可以使用is_nan()函数来判断数值是否是Nan。
序列
序列规则:
- 1到4,不包括5 :1...5
- 1到5,包括5:1...=5
- 从0到4:...5
- 从0到结束:0...
- 从开始到结束:...
常用于循环遍历:
rust
for i in 1..5
{}
字符/布尔/单元类型
字符使用单引号' '来表示。在rust中,字符使用Unicode编码,大小为4个字节。
Rust 中的布尔类型有两个可能的值:true
和 false
。大小为1个字节。
单元类型就是(),例如,fn main函数的返回值就是单元类型。单元类型不占内存。
其他类型
字符串和切片
字符串
字符串是由字符组成的连续集合,需要注意的是,Rust中的字符是 Unicode 类型,因此每个字符占据 4 个字节 内存空间,但是在字符串中不一样,字符串是 UTF-8 编码,也就是字符串中的字符所占的字节数是变化的(1 - 4)。
rust
let s = String::from("hello world");//基础库中常用的字符串为String
let s1:&str = "hello world"; //字符串字面量的类型为&str
//两者转换
let s2:&str = &s;
let s3:&str = &s[..];
let s4:String = "hello,world".to_string()
字符串的底层的数据存储格式实际上是[ u8
]。因为字符串中每个字符所占的字节数不一致,所以没有办法使用整型索引(切片亦是如此,遇到含中文的字符串可能会崩溃)。
String是可变字符串,下面罗列下其添加、修改、删除时用到的方法。
rust
//追加
let mut s = String::from("Hello ");
s.push_str("rust");
s.push('!');
//插入
let mut s = String::from("Hello rust!");
s.insert(5, ',');//参数1为插入索引
s.insert_str(6, " I like");
//替换
//replace,该方法是返回一个新的字符串,而不是操作原来的字符串。
let string_replace = String::from("I like rust. Learning rust is my favorite!");
let new_string_replace = string_replace.replace("rust" /*old*/, "RUST" /*new*/);
//replacen,该方法是返回一个新的字符串,而不是操作原来的字符串。
let string_replace = "I like rust. Learning rust is my favorite!";
let new_string_replacen = string_replace.replacen("rust", "RUST", 1 /*替换个数*/);
//replace_range,该方法是直接操作原来的字符串,不会返回新的字符串
let mut string_replace_range = String::from("I like rust!");
string_replace_range.replace_range(7..8 /*replace range*/, "R" /*new*/);
//删除
//pop,该方法是直接操作原来的字符串。但是存在返回值,其返回值是一个 Option 类型
let mut string_pop = String::from("rust pop 中文!");
let p1 = string_pop.pop();//p1 Some('!')
let p2 = string_pop.pop();//p2 Some('文'),此时string_pop为"rust pop 中"
//remove,该方法是直接操作原来的字符串,删除并返回字符串中指定位置的字符,按照字节来处理字符串的
let mut string_remove = String::from("测试remove方法");
string_remove.remove(0);// 删除第一个汉字
string_remove.remove(1);//代码会发生错误 Error
string_remove.remove(3);// 直接删除第二个汉字
//truncate,该方法是直接操作原来的字符串,删除字符串中从指定位置开始到结尾的全部字符,按照字节来处理字符串的
let mut string_truncate = String::from("测试truncate");
string_truncate.truncate(3);
//clear,该方法是直接操作原来的字符串,清空字符串
let mut string_clear = String::from("string clear");
string_clear.clear();
//连接
//使用 + 或者 += 连接字符串,要求右边的参数必须为字符串的切片引用(Slice)类型,+ 是返回一个新的字符串
let string_append = String::from("hello ");
let string_rust = String::from("rust");
let result = string_append + &string_rust;// &string_rust会自动解引用为&str;并且此时string_append 的所有权会被转移到函数的参数1上,后续无法使用该变量
let mut result = result + "!"; // ' result + "!" '中的 'result' 是不可变的
result += "!!!";
//add函数,上面的+号就相当于调用了add函数,参考c++函数符重载
fn add(self, s: &str) -> String
//format!
let s1 = "hello";
let s2 = String::from("rust");
let s = format!("{} {}!", s1, s2);
遍历字符串,如下。
rust
//以字符的形式
for c in "中国人".chars(){}
//以字节的形式
for b in "中国人".bytes(){}
切片
切片允许你引用集合中部分连续的元素序列,而不是引用整个集合。切片的使用方式:
rust
let s = String::from("hello world");
let s1 = &s[0..5];//[开始索引..终止索引),右半开区间,见序列规则。使用切片必须带&引用
//注意对字符串切片时语法要格外小心,切片的索引必须落在字符边界的位置,即UTF-8字符边界,如下中文在utf-8中占用三个字节,下面代码会崩溃
let s = "中国人";
let s1 = &s[0..2];//只取前两个字节,但是每个汉字占用三个字节
//**在rust中因为对程序员屏蔽了内存层,在使用时尤其注意深复制和浅复制问题,切片是使用的原字符串,可以认为是一种浅复制**
//当字符串被切片后,字符串的生命周期应该大于切片的声明周期
let s = Stirng::from("hello");
let s1 = &s[0..2];
s.clear();
println!("s1 : {}", s1); //error
字符串的切片的类型为&str
。切片不是字符串独有的,其他集合类型也有,例如数组。
结构体
结构体的定义、赋值和访问:
rust
struct User {
active: bool,
username: String,
sign_in_count: u64,
}
//赋值时,采用key:value的形式
let mut user1 = User {
username: String::from("someusername123"),
active: true,
sign_in_count: 1,
};
//需要注意的是,必须要将结构体实例声明为可变的,才能修改其中的字段,Rust不支持将某个结构体某个字段标记为可变。
user1.username = String::from("someusername456");
//使用user1给user2赋值,其中user1的username字段所有权会转移到user2中
let user2 = User{
active:false,
..user1
};
元组结构体,结构体必须要有名称,但是结构体的字段可以没有名称,这种结构体长得很像元组,因此被称为元组结构体,如下:
rust
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
单元结构体,没有任何字段和属性,如果你定义一个类型,但是不关心该类型的内容, 只关心它的行为时,就可以使用单元结构体,如下:
rust
struct AlwaysEqual;
impl SomeTrait for AlwaysEqual{}
在处理结构体所有权
时需要注意,如果想在结构体中使用一个引用
,就必须加上生命周期
,否则就会报错。
枚举类型
枚举(enum)允许你通过列举可能的成员来定义一个枚举类型,例如扑克牌花色:
rust
//与c枚举最大的不同,每种枚举类型都可以定义自己的数据类型。使用过程中只要牢记,不管样子怎么变,变量都是两大核心:类型和值
enum PokerCard {
Clubs(u8),
Spades(u8),
Diamonds(char),
Hearts(char),
}
//访问枚举值
let heart = PokerSuit::Hearts(10);
枚举类型
是一个类型,它会包含所有可能的枚举成员(如上述牌花色); 而枚举值
是该类型中的具体某个成员的实例(如方块花色10)。
rust中常用的枚举:
-
Option枚举用于处理空值
rustenum Option<T> { Some(T), None, } enum Result<T, E> { Ok(T), Err(E), }
数组
Rust 中,最常用的数组有两种,第一种是速度很快但是长度固定的 array
,第二种是可动态增长的但是有性能损耗的 Vector
。称 array
为数组,Vector
为动态数组。
数组的使用:
rust
//定义数组
let a = [1, 2, 3, 4, 5];
let a:[i32;5] = [1,2,3,4,5];
let a = [3;5]//五个三的数组
//访问数组元素
//索引访问(如果越界访问会造成程序崩溃)
let first = a[0];
let second = a[1];
和字符串一样,数组也可以使用切片(创建切片的代价非常小,因为切片只是底层数组的一个引用),如下:
rust
let a: [i32; 5] = [1, 2, 3, 4, 5];
let slice: &[i32] = &a[1..3];//&[i32]为数组切片类型(切片可以认为是数组的引用)
需要注意的是,[u8;3]和[u8;4]是两种不同的类型,数组的长度也是类型的一部分
。
元组
元组是由多种类型组合到一起形成的,因此它是复合类型,元组的长度是固定的,元组中元素的顺序也是固定的。如下:
rust
let tup: (i32, f64, u8) = (500, 6.4, 1);
let tup = (500, 6.4, 1);
//用模式匹配来解构元组
let (x, y, z) = tup;
//用点来访问元组,元组索引从0开始
let x = tup.0;//500
let y = tup.1;//6.4
元组有一个特别常用的场景,在函数的返回值可以使用一个元组返回多个值。
引用
引用的使用方式:
rust
let x = 5;
let y : &int = &x; //引用默认也是不可变的,不可变引用可以同时存在多个
//声明可变引用(★重点:为了避免数据竞争,同一个作用域,只能同时存在一个可变引用,与不可变引用也不能同时存在;即同一作用域如果存在一个可变引用,就不能再存在其他引用)
let z = &mut x;
//y是一个引用类型,可以对y解引用
assert_eq!(5,*y);
//直接用y和5做比较是错误的,类型不匹配
if y == 5 {} //complier error
值得注意的是,引用的作用域和变量不同:引用的作用域从创建开始,一直持续到它最后一次使用的地方;变量的作用域持续到某个花括号结束。
函数
1.函数声明
rust
fn NAME (VAR:type)->type {}
//ep:
fn add (var1:i32, var2:i32)->i32
{
if var1 > var2 { return var1 } //return提前返回
var1+var2
}
注意:函数的每个参数都需要标注类型
。rust中函数即是表达式。
2.函数的返回值
如果没有显示强调函数的返回值,函数默认返回空元组(),以下两种情况都会返回():
- 函数没有返回值
- 函数通过';'结尾
当使用!作为函数返回类型的时候,表示该函数永不返回。
方法
Rust 的方法可以跟结构体、枚举、特征(Trait)一起使用。Rust 使用 impl
来定义方法,例如以下代码:
rust
struct Rect {
x:i64,
y:i64,
};
//给结构体Circle实现方法
impl Rectangle{
// new是Circle的关联函数,这种方法往往用于初始化当前结构体的实例,使用Rect::new(x,y)的形式调用
fn new(x:i64, y:i64)->Circle{
Rect {x:x, y:y}
}
//&self其实是self: &Self的简写,在一个 impl块内,Self指代被实现方法的结构体类型,self 指代此类型的实例
fn area(&self) -> u32 {
self.x * self.y
}
}
}
需要注意的是,self
依然有所有权的概念:
self
表示Rectangle
的所有权转移到该方法中,这种形式用的较少&self
表示该方法对Rectangle
的不可变借用&mut self
表示可变借用
定义在 impl
中且没有 self
的函数被称之为关联函数, ::
语法用于访问关联函数和模块创建的命名空间。
泛型和特征
泛型
类似于c++语言的中的模板,主要目的是为程序员提供编程的便利,减少代码的臃肿。其使用方式如下:
rust
//使用泛型参数,有一个先决条件,必需在使用前对其进行声明,largest<T>
fn largest<T>(list: &[T]) -> T {
let mut largest = list[0];
for &item in list.iter() {
//这段代码没法编译成功,因为不是所有类型都能使用>,所以需要用特征做限制,参考后续的特征
if item > largest {
largest = item;
}
}
largest
}
//结构体使用泛型
struct Point<T, E>{
x:T,
y:E,
}
//枚举中使用泛型
enum Option<T>{
Some(T),
None,
}
//方法中使用泛型
struct Point<T> {
x: T,
y: T,
}
impl <T> Point <T>{ //使用泛型前依旧需要提前声明impl<T>,此时Point<T>是完整的类型
fn x(&self) -> &T {
&self.x
}
}
//针对具体的类型实现方法(参考c++模板具体化)
impl Point<i32>{
fn distance_from_origin(&self) -> f32 {
(self.x.powi(2) + self.y.powi(2)).sqrt()
}
}
//const泛型
const N :usize; //表示const泛型N ,它基于的值类型是usize
fn display_array<T: std::fmt::Debug, const N: usize>(arr: [T; N]) {
println!("{:?}", arr);
}
fn main() {
let arr: [i32; 3] = [1, 2, 3];
display_array(arr);
let arr: [i32; 2] = [1, 2];
display_array(arr);
}
特征
特征(trait)定义了一组可以被共享的行为,只要实现了特征,你就能使用这组行为。特征跟接口的概念类似。其使用方式如下:
rust
trait trait_name{
fn fn1()->();//特征只定义行为看起来是什么样的,而不定义行为具体是怎么样的。因此,只需要定义特征方法的签名
}
//为类型实现具体的特征
impl trait_name for type{
fn fn1()->(){}
}
关于特征实现与定义的位置,有一条非常重要的原则(孤儿规则):如果你想要为类型 A
实现特征 T
,那么 A
或者 T
**至少有一个是在当前作用域中定义的!**例如,无法为 String
类型实现 Display
特征,他俩都是标准库中定义的,和你没有任何关系。
特征和接口最大的不同之处在于,特征可以为其方法定义默认实现
。
rust
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Read more...)")
}
}
特征的作用:做函数参数。
rust
//你可以使用任何实现了 Summary 特征的类型作为该函数的参数,同时在函数体内,还可以调用该特征的方法
fn notify(item:&impl Summary){
...
}
//特征做函数参数的另一种表示形式(和泛型很像,可以想象成约束性泛型),称特征约束T:Summary
fn notiy<T:Summary>(item:&T){
...
}
//多重约束,可以指定多个约束条件,例如除了让参数实现 Summary 特征外,还可以让参数实现 Display 特征以控制它的格式化输出
fn notify(item:impl &(Summary + Display)){}
fn notify<T:Summary+Display>(item:&T){}
//Where约束(语法糖)
fn notify<T>(item:&T)
where T:Summary+Dispaly
{}
使用特征约束可以有条件的实现方法或者特征。
rust
struct Pair<T> {
x: T,
y: T,
}
//只有 T 同时实现了 Display + PartialOrd 的 Pair<T> 才可以拥有此方法
impl<T: Display + PartialOrd> Pair<T> {
fn cmp_display(&self)
{}
}
//为任何实现了 Display 特征的类型实现了 ToString 特征
impl <T:Display> ToString for T
{}
函数中可以返回一个实现了某个特征的类型。
rust
fn return_summary()->impl Summary{}//这种方式有一个特别大的限制,只能有一个具体的类型
通过derive派生特征。例如:
rust
#[derive(Debug)]//一种特征派生语法,被 derive 标记的对象会自动实现对应的默认特征代码,继承相应的功能。
特征对象
特征对象是指实现了某个特征的实例,特征对象由dyn修饰,可以用使用引用或者Box<T>智能指针的方式创建。
rust
fn func(x:&dyn Summary){}
fn func(x:Box<dyn Summary>){}
//注意 dyn 不能单独作为特征对象的定义,例如下面的代码编译器会报错,原因是特征对象可以是任意实现了某个特征的类型,编译器在编译期不知道该类型的大小,不同的类型大小是不同的。
//而 &dyn 和 Box<dyn> 在编译期都是已知大小,所以可以用作特征对象的定义。
fn func(x: dyn Summary) {}//报错
使用特征约束和特征对象的区别:
rust
struct Test<T:Summary>{
a:Vec<T>, //特征约束,这里的vec数组中只能存取同一种类型
}
struct Test{
a:Vec<Box<dyn Summary>>, //特征对象,这里的vec能存取所有实现了summary的类型
}
特征对象使用的是动态转发(运行时才确定调用的是什么方法,关键字dyn修饰),而泛型使用的是静态分发(编译期完成处理)。
特征对象的限制(只有满足如下要求时,特征才是安全的,才可以拥有特征对象):
- 方法的返回类型不能是
Self
- 方法没有任何泛型参数
关联类型,是在特征定义的语句块中,申明一个自定义类型,这样就可以在特征的方法签名中使用该类型:
rust
trait Add<RHS=Self> { //RHS=Self为默认泛型类型参数
type Output;//关联类型,在实现特征时使用type Output = ojbtype;为其赋值
fn add(self, rhs: RHS) -> Self::Output; //注意,这里和特征对象限制并不冲突,我需要知道的是Output的类型,而不是Self本身的类型
}
//如上所示,当使用泛型类型参数时,可以为其指定一个默认的具体类型
//泛型也可以实现关联类型的效果,但是会使得程序更加复杂(实现特征时语法更加复杂)
trait Add<T>{
fn add(&self, right:T)->Option<T>;
}
特征定义中的特征约束,让某个特征 A 能使用另一个特征 B 的功能(另一种形式的特征约束),这种情况下,不仅仅要为类型实现特征 A,还要为类型实现特征 B 才行,如下:
rust
trait OutlinePrint: Display {//OutlinePrint: Display为特征中定义特征约束的语法
fn outPirnt(&self){}
}
完全限定语法:
rust
<Type as Trait>::function(receiver_if_method, next_arg, ...);
复合类型
动态数组
动态数组类型用 Vec<T>
表示,它允许你存储多个值,但是只能存储相同类型的元素(和C++vector一样)。
创建和使用动态数组的方式,如下:
rust
let v:Vec<i32> = Vec::new();
//使用宏vec!创建数组,可以赋予初始化值
let mut v = vec![1,2,3];
//创建并指定容量
Vec::with_capacity(capacity);
//尾部添加元素
v.push(4);
//使用索引访问数组
let var = &v[2];//访问数据3
//使用get函数访问数组
let var = v.get(2);//不同于索引的是,get返回的是Option<&T>类型
//同时借用多个数组问题(以下这段代码无法编译成功,因为同时存在可变引用和不可变引用)
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
//迭代数组(使用mut标志,迭代的同时可以修改数组值)
for i in &mut v{
...
}
映射
HashMap
也是 Rust 标准库中提供的集合类型,类比C++中的map。主要做键值对映射存储,提高查询速率。
HashMap的创建和使用方式,如下:
rust
//创建一个map
let mut map1 = HashMap::new();
//将key和value插入到map中
map1.insert("key", "value");//map1为HashMap<&str, &str>类型
//将元组数组转为hashmap
let teams_list = vec![
("中国队".to_string(), 100),
("美国队".to_string(), 10),
];
let teams_map: HashMap<_,_> = teams_list.into_iter().collect();
//查询map中的值
let score:Option<&i32> = teams_map.get("中国队");
//查询时的注意点:
//1.get 方法返回一个 Option<&i32> 类型:当查询不到时,会返回一个 None,查询到时返回 Some(&i32)
//2.&i32 是对 HashMap 中值的借用,如果不使用借用,可能会发生所有权的转移
//查询值,若不存在则插入,若存在则不会插入;并返回value的值
let v = teams_map.entry("日本队").or_insert(5);
//or_insert 返回了 &mut v 引用,因此可以通过该可变引用直接修改 map 中对应的值
//使用 count 引用时,需要先进行解引用 *count,否则会出现类型不匹配
*v += 1;
//循环遍历map
for(key, value) in &teams_list{
//key - value
}
HashMap
的所有权规则(下面2条是所有权核心规则)与其它 Rust 类型没有区别:
- 若类型实现
Copy
特征(基本类型都有这个特征),该类型会被复制进HashMap
,因此无所谓所有权。 - 若没实现
Copy
特征,所有权将被转移给HashMap
中。
类型转换
类型转换必须是显式的。Rust 永远也不会偷偷的把16bit 整数转换成 32bit 整数。
- as转换
rust
fn main() {
let a = 3.1 as i8;
let b = 100_i8 as i32;
let c = 'a' as u8; // 将字符'a'转换为整数,97
if (a as i32) < b {}
println!("{},{},{}",a,b,c)
}
- tryinto转换(可以捕捉类型由大到小溢出错误)
rust
fn main() {
let b: i16 = 1500;
let b_: u8 = match b.try_into() {
Ok(b1) => b1,
Err(e) => {
println!("{:?}", e.to_string());
0
}
};
}
注意:对于某种类型来说,例如i32,其i32、&i32、&mut i32、mut i32都表示不同的类型。
Rust 中常见的 DST(动态大小)
类型有: str
、[T]
、dyn Trait
,它们都无法单独被使用,必须要通过引用或者 Box
来间接使用 。
全局变量
编译期初始化
-
静态常量
rust//必须指明类型 const MAX_ID: usize = usize::MAX / 2; fn main() { println!("用户ID允许的最大值是{}",MAX_ID); }
-
静态变量
ruststatic mut REQUEST_RECV: usize = 0; fn main() { unsafe { //Rust 要求必须使用unsafe语句块才能访问和修改static变量 REQUEST_RECV += 1; assert_eq!(REQUEST_RECV, 1); } }
静态变量和常量的区别
- 静态变量不会被内联,在整个程序中,静态变量只有一个实例,所有的引用都会指向同一个地址。
- 存储在静态变量中的值必须要实现 Sync trait。
运行期初始化
静态初始化有一个致命的问题:无法用函数进行静态初始化,例如你如果想声明一个全局的Mutex锁:
rust
static NAMES: Mutex<String> = Mutex::new(String::from("Sunface, Jack, Allen")); //会报错,静态变量没法使用函数初始化
-
通过使用lazy_static包来解决这个问题,lazy_static是社区提供的非常强大的宏,用于懒初始化静态变量,之前的静态变量都是在编译期初始化的,因此无法使用函数调用进行赋值,而lazy_static允许我们在运行期初始化静态变量!
rustuse std::sync::Mutex; use lazy_static::lazy_static; lazy_static! { //lazy_static宏,匹配的是static ref,所以定义的静态变量都是不可变引用 static ref NAMES: Mutex<String> = Mutex::new(String::from("Sunface, Jack, Allen")); } fn main() { let mut v = NAMES.lock().unwrap(); v.push_str(", Myth"); println!("{}",v); }
-
Box::leak,将一个变量从内存中泄漏,然后将其变为'static生命周期。
打印问题
常用的三个打印函数print!、println!、format!,如下:
print!
将格式化文本输出到标准输出,不带换行符println!
同上,但是在行的末尾添加换行符format!
将格式化文本输出到String
字符串
rust
let s = "hello";
println!("{}, world", s); //占位符{},会被后面的参数依次替换
let s1 = format!("{}, world", s);
print!("{}", s1);
print!("{}\n", "!");
- Debug特征,对于数值、字符串、数组,可以直接使用
{:?}
或{:#?}
(更加优雅)进行输出。但是对于结构体,需要派生Debug特征后,才能进行输出。 - Display特征,实现了Display特征后,可以直接使用{ }进行输出。
流程控制
-
条件选择
rustif contion {} else if contion {} else {}
-
循环
rust//for var in [&] [mut] list,rust for循环主要用作对各种数据集合的遍历(注意,因为所有权问题,使用 for时我们往往使用集合的引用形式)。 for item in [&][mut] collection{} //在Rust 中 _ 的含义是忽略该值或者类型的意思 for _ in 0..10 {} //while循环,根据循环条件来决定是否继续循环 while condition {} //loop循环,简单的无限循环 loop {} //continue 同c语言 //break 区别c语言的地方是break可以带返回值 break xxx;
模式匹配
match
在 Rust 中,模式匹配最常用的就是 match
(类比c语言中的switch,不同的是match可以对模式匹配,配合枚举类型使用,是match的核心所在,并且match可以有返回值) 和 if let
。
match的使用,如下:
rust
//语法
match target {
模式1 => {表达式1},
模式2| 模式3 => {表达式2},
_ =>{表达式3}, //其他
};
//个人理解,这里的模式匹配可以理解为类型匹配,通过对比是否符合枚举类型来做匹配
//例子
enum Direction {
East,
West,
North{a:i16},
South(i16),
}
fn main() {
let dire = Direction::South;
match dire {
Direction::East => println!("East"),
Direction::North | Direction::West => {
println!("West or North");
},
_ => println!("West"),
};
let dirt_get_value = Direction::South(20);
match dirt_get_value{
Direction::South(val) => {
//匹配,并且获取模式绑定的值。注意如果模式中使用的是结构体匹配,那么获取绑定值变量名需要和枚举值中结构体成员名相同,
//例如,枚举类型中枚举值为North{a:i16},那么模式为Direction::North{a}
println!("South and get value {}", val);
},
}
}
使用match时,需要注意以下几个问题:
match
的匹配必须要穷举出所有可能,因此这里用 _ 来代表未列出的所有可能性。match
的每一个分支都必须是一个表达式,且所有分支的表达式最终返回值的类型必须相同
。- X|Y,类似逻辑运算符
或
,代表该分支可以匹配X
也可以匹配Y
,只要满足一个即可
if let
只有一个模式的值需要被处理时使用match会显得很冗余,rust提供了if let的方式,如下:
rust
if let mode = target {...}
模式
模式是 Rust 中的特殊语法,它用来匹配类型中的结构和数据,它往往和 match 表达式联用,以实现强大的模式匹配能力。模式一般有以下的表达形式 :
- 字面值模式匹配
rust
let x = 1;
match x {
1 => println!("one"),
2 => println!("two"),
_ => println!("anything"),
}
- 解构的数组、枚举、结构体或者元组模式匹配
rust
struct Point {
x: i32,
y: i32,
}
fn main() {
let p = Point { x: 0, y: 7 };
//结构体解构
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解构
match p {
Point { x, y } => println!("On neither axis: ({}, {})", x, y),
//元组解构
let ((feet, inches), Point {x, y}) = ((3, 10), Point { x: 3, y: -10 });
}
- 变量模式匹配
rust
fn main() {
let x = Some(5);
match x {
Some(y) => println!("Matched, y = {:?}", y),
_ => println!("Default case"),
}
}
- 通配符模式匹配
- 占位符模式匹配
rust
//可以在一个模式内部使用 _ 忽略部分值
let numbers = (2, 4, 8, 16, 32);
match numbers {
(first, _, third, _, fifth) => {
println!("Some numbers: {}, {}, {}", first, third, fifth)
},
}
//.. 模式会忽略模式中剩余的任何没有显式匹配的值部分
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),
}
模式匹配需要通过例子来帮助理解,有关模式的详细介绍可以参考rust圣经中的模式匹配
章节。
解构式赋值
在 Rust 1.59 版本后,可以在赋值语句的左值中使用元组、切片和结构体模式。
rust
struct Struct {
e: i32
}
fn main() {
let (a, b, c, d, e);
(a, b) = (1, 2); //第一次赋值(绑定),后面继续对a、b赋值会报错,因为其为非mut
// _ 代表匹配一个值,但是我们不关心具体的值是什么,因此没有使用一个变量名而是使用了 _
[c, .., d, _] = [1, 2, 3, 4, 5];
Struct { e, .. } = Struct { e: 5 };
assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]);
}
项目构建
rust从高层次到低层次管理项目分别是:软件包(packages)、单元包(crate)、模块(module)
包(packages):Package 就是一个项目(项目包),因此它包含有独立的 Cargo.toml 文件,以及因为功能性被组织在一起的一个或多个单元包。一个 Package 只能包含一个库(library)类型的单元包
,但是可以包含多个二进制可执行类型的单元包
。
单元包(crate):对于 Rust 而言,单元包是一个独立的可编译单元,它编译后会生成一个可执行文件
或者一个库
。
模块(Module):可以一个文件多个模块,也可以一个文件一个模块,模块可以被认为是真实项目中的代码组织单元
注意,Package 是一个项目工程,而crate只是一个编译单元;Package中包含一个或多个crate,且至少包含一个同名的crate。
记:
1.Package开始是顶级目录,其他模块中引用或者使用use引用时,使用绝对路径时,从顶级目录开始(以包名或'crate'开始)。
2.package中的src/main.rs 和 src/lib.rs 被称为包根(crate root,单元包树形结构的根部)。
模块
模块使用mod关键字标识,可以嵌套声明,如下:
rust
//pub 关键字可以控制模块和模块中指定项的可见性。
pub mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
pub mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
在lib.rs中声明如上三个模块,则可形成如下的模块树形结构:
想要调用一个函数,就需要知道它的路径,在 Rust 中,这种路径有两种形式:
- 绝对路径 ,从包根开始,路径名以
包名
或者crate
作为开头 - 相对路径 ,从当前模块开始,以
self
,super
或当前模块的标识符
作为开头
每一层使用::分开,例如,crate::front_of_house::hosting::add_to_waitlist()。
super代表的是父模块为开始的引用方式,非常类似于文件系统中的 ..
语法:.../a/a.txt。
self引用自身模块中的项,相当于.
语法:./a.txt。
模块需要注意的问题:
1.在rust中,子模块可以访问父模块、父父模块...的私有项,但是父模块无法访问子模块的私有项。
2.同一层级的模块可以相互访问(例如,属于同一个包根作用域下的两个模块可以相互访问)。
3.模块可以是同名的文件,或是同名的文件夹。
如果需要将文件夹作为一个模块,我们需要进行显示指定暴露哪些子模块,有两种方法::
- 在 子模块目录里创建一个
mod.rs
,如果你使用的rustc
版本1.30
之前,这是唯一的方法。 - 在 子模块同级目录里创建一个与模块(目录)同名的 rs 文件
front_of_house.rs
,在新版本里,更建议使用这样的命名方式来避免项目中存在大量同名的mod.rs
文件。
而无论是上述哪个方式创建的文件,其内容都是一样的,在文件中定义需要暴露的子模块。
rust
pub mod son_mod_name; //类似于c中的include<mod>
将其他模块引入到当前程序中:
rust
//通过mod引入,告诉编译器去加载和编译指定的模块文件,并将其作为当前模块的子模块,需要使用完整的路径来访问这些项。
mod front_of_house;
crate::front_of_house::hosting::add_to_waitlist();
//通过use引入模块,可以引入模块中具体的项
use crate::front_of_house::hosting;
hosting::add_to_waitlist();
//使用as来为引入的项取别名
use crate::front_of_house::hosting as othername;
othername::add_to_waitlist();
引入项再导出:引入项再导出的主要目的是在一个模块中重新导出来自其他模块的项,以便在使用该模块的代码中可以直接访问这些项,而无需通过中间模块的路径来访问。使用的关键字是
'pub use'。
如何引入第三方依赖:
1.修改 Cargo.toml
文件,在 [dependencies]
区域添加一行:rand = "0.8.3"
。。
2.如果用的是 VSCode
和 rust-analyzer
插件,该插件会自动拉取该库,等它完成后,再进行下一步(VSCode 左下角有提示)。
3.通过using在代码中使用第三方依赖。
限制可见性语法:
pub
意味着可见性无任何限制pub(crate)
表示在当前包可见pub(self)
在当前模块可见pub(super)
在父模块可见pub(in <path>)
表示在某个路径代表的模块中可见,其中path
必须是父模块或者祖先模块
cargo
Rust的项目主要分为两种类型:bin和lib
rust
//创建项目
cargo new project_name --bin
cargo new lib_name --lib
cargo.toml和cargo.lock是cargo的核心文件。
创建项目后,会生成一个用作项目配置的Cargo.toml文件。
Cargo.lock 文件是 cargo 工具根据同一项目的 toml 文件生成的项目依赖详细清单,一般不用修改它。
在cargo.toml中,主要通过各种依赖段落来描述该项目的各种依赖项:
- 基于 Rust 官方仓库 crates.io,通过版本说明来描述
- 基于项目源代码的 git 仓库地址,通过 URL 来描述
- 基于本地项目的绝对路径或者相对路径,通过类 Unix 模式的路径来描述
rust
//cargo.toml中描述依赖项
[dependencies]
rand = "0.3"
hammer = { version = "0.5.0"}
color = { git = "https://github.com/bjz/color-rs" }
geometry = { path = "crates/geometry" }
rust
//编译和运行项目
cargo bulid [--release]
cargo run [--release]
//快速检查代码的正确性
cargo check
高级用法
函数式编程
闭包
闭包是一种匿名函数,它可以赋值给变量也可以作为参数传递给其它函数
,不同于函数的是,它允许捕获调用者作用域中的值
。并且在rust中每一个闭包实例都有独属于自己的类型,即使于两个签名一模一样的闭包,它们的类型也是不同的。
闭包的声明形式(闭包无需标注参数和返回值的类型,可以自行推导):
rust
|param1, param2,...|-> type {
语句1;
返回表达式
}
//函数
fn add_one_v1 (x: u32) -> u32 { x + 1 }
//闭包的几种写法
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x| { x + 1 };
let add_one_v4 = |x| x + 1 ;
//move关键字强制闭包取得捕获变量的所有权(闭包捕获变量时,优先使用copy,无法使用copy时,才会用所有权转移)
let add_one_v3 = move|x| { x + 1 };
三种Fn特征,闭包捕获变量有三种途径,恰好对应函数参数的三种传入方式:转移所有权、可变借用、不可变借用,因此相应的 Fn
特征也有三种:
- FnOnce,该类型的闭包会拿走被捕获变量的所有权。
- FnMut,它以可变借用的方式捕获了环境中的值。
- Fn 特征,它以不可变借用的方式捕获环境中的值。
注意,一个闭包实现了哪种 Fn 特征取决于该闭包如何使用被捕获的变量,而不是取决于闭包如何捕获它们。
迭代器
IntoIterator特征
在rust中,为某个集合类型实现了IntoIterator特征后,就可以通过for语法糖自动把实现了该特征的集合类型转换为迭代器。
rust
let arr = [1, 2, 3]; //数组实现了IntoIterator特征
for v in arr { //自动转换为迭代器进行访问
println!("{}",v);
}
//IntoIterator特征原型
pub trait IntoIterator {
type Item;
type IntoIter: Iterator<Item = Self::Item>;
fn into_iter(self) -> Self::IntoIter; //通过该方法,实现了该特征的集合类型可以返回对应的迭代器
}
into_iter、iter和iter_mut方法,它们都可以显式的把集合对象转换成迭代器,例如:
rust
let arr = [1, 2, 3];
for v in arr.into_iter() {
println!("{}", v);
}
这三者的区别是:
- into_iter 会夺走所有权。
- iter 是借用,实现的迭代器调用next返回的类型是Some(&T)。
- iter_mut 是可变借用,实现的迭代器调用next返回的类型是Some(&mut T)。
Iterator特征
rust
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>; //next方法控制如何从集合中取值
//省略其余有默认实现的方法...
}
在rust中,迭代器是惰性的,如果不使用它,将不会发生任何事:
rust
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
//在for循环之前,只是简单的创建了一个迭代器 v1_iter,此时不会发生任何迭代行为
//惰性初始化的方式确保了创建迭代器不会有任何额外的性能损耗,其中的元素也不会被消耗
for val in v1_iter {
println!("{}", val);
}
消费者适配器
只要迭代器上的某个方法A在其内部调用了next方法,那么A就被称为消费性适配器:因为next方法会消耗掉迭代器上的元素,所以方法A的调用也会消耗掉迭代器上的元素。例如下面的sum函数:
rust
fn main() {
let v1 = vec![1, 2, 3];
let v1_iter = v1.iter();
let total: i32 = v1_iter.sum();
assert_eq!(total, 6);
}
迭代器适配器
消费者适配器是消费掉迭代器,然后返回一个值。而迭代器适配器,是返回一个新的迭代器,这是实现链式方法调用的关键。与消费者适配器不同,迭代器适配器是惰性的,因此需要一个消费者适配器来收尾,最终将迭代器转换成一个具体的值:
rust
let v1: Vec<i32> = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();//map方法是一个迭代者适配器,它是惰性的,不产生任何行为,因此还需要一个消费者适配器collect进行收尾
assert_eq!(v2, vec![2, 3, 4]);
智能指针
Box<T>堆对象分配
Box<T>
允许你将一个值分配到堆上,然后在栈上保留一个智能指针指向堆上的数据。
相比其它语言,Rust 堆上对象还有一个特殊之处,它们都拥有一个所有者,因此受所有权规则的限制:当赋值时,发生的是所有权的转移(只需浅拷贝栈上的引用或智能指针即可
),例如以下代码:
rust
fn main() {
let b = foo("world");
println!("{}", b);
}
fn foo(x: &str) -> String {
let a = "Hello, ".to_string() + x;
a //a的所有权返回给b
}
Box使用场景:
-
使用 Box<T> 将数据存储在堆上。
rustfn main() { let a = Box::new(3); println!("a = {}", a); // a = 3 // 下面一行代码将报错 // let b = a + 1; // cannot add `{integer}` to `Box<{integer}>` //正确的写法是 let b = *a + 1; }
-
避免栈上数据的拷贝。
rustfn main() { // 在栈上创建一个长度为1000的数组 let arr = [0;1000]; // 将arr所有权转移arr1,由于 `arr` 分配在栈上,因此这里实际上是直接重新深拷贝了一份数据 let arr1 = arr; // arr 和 arr1 都拥有各自的栈上数组,因此不会报错 println!("{:?}", arr.len()); println!("{:?}", arr1.len()); // 在堆上创建一个长度为1000的数组,然后使用一个智能指针指向它 let arr = Box::new([0;1000]); // 将堆上数组的所有权转移给 arr1,由于数据在堆上,因此仅仅拷贝了智能指针的结构体,底层数据并没有被拷贝 // 所有权顺利转移给 arr1,arr 不再拥有所有权 let arr1 = arr; println!("{:?}", arr1.len()); // 由于 arr 不再拥有底层数组的所有权,因此下面代码将报错 // println!("{:?}", arr.len()); }
-
将动态大小类型变为 Sized 固定大小类型。
rustenum List { Cons(i32, Box<List>), //如果这里直接使用List会报错,因为无法知道具体的大小 Nil, }
-
特征对象。
rusttrait Draw { fn draw(&self); } impl Draw for Button { fn draw(&self) { println!("这是屏幕上第{}号按钮", self.id) } } struct Select { id: u32, } impl Draw for Select { fn draw(&self) { println!("这个选择框贼难用{}", self.id) } } fn main() { let elems: Vec<Box<dyn Draw>> = vec![Box::new(Button { id: 1 }), Box::new(Select { id: 2 })]; for e in elems { e.draw() } }
-
Box::leak
Box 中还提供了一个非常有用的关联函数:
Box::leak
,它可以消费掉Box
并且强制目标值从内存中泄漏。例如,将一个String类型字符串的生命周期提升为'static。
Deref解引用
Deref特征可以让智能指针像引用那样工作,这样就可以写出同时支持智能指针和引用的代码,例如*T(参考运算符重载)。还可以连续的实现如Box<String> -> String -> &str的隐式转换,只要链条上的类型实现了Deref特征。
Deref解引用规则:
- 一个类型为T的对象foo,如果
T: Deref<Target=U>
,那么,相关foo
的引用&foo
在应用的时候会自动转换为&U
。 - Rust 会在解引用时自动把智能指针和
&&&&v
做引用归一化操作,转换成&v
形式,最终再对&v
进行解引用。
三种Deref的转换:
- 当T: Deref<Target=U>,可以将 &T 转换成 &U。
- 当T: Deref<Target=U>,可以将&mut T转换成&U。
- 当T: DerefMut<Target=U>,可以将&mut T转换成&mut U。
Drop释放资源
在rust中当一个变量超过作用域范围后,就会通过Drop特征来回收资源。
Drop的一些注意点:
-
Drop特征中的drop方法借用了目标的可变引用,而不是拿走了所有权。
rustpub trait Drop { fn drop(&mut self); }
-
结构体中每个字段都有自己的Drop。
Drop的顺序:
- 变量级别,按照逆序的方式。
- 结构体内部,按照顺序的方式。
注意,无法为一个类型同时实现 Copy 和 Drop
特征。因为实现了 Copy
的特征会被编译器隐式的复制,导致非常难以预测析构函数执行的时间和频率。因此这些实现了 Copy
的类型无法拥有析构函数。
Rc与Arc引用计数
rust通过引用计数的方式,允许一个数据资源在同一时刻拥有多个所有者。这种实现机制就是 Rc
和 Arc
,前者适用于单线程,后者适用于多线程(原子化实现的引用计数,因此是线程安全的)。这两者都是只读的,如果想要实现内部数据可修改,必须配合内部可变性 RefCell 或者互斥锁 Mutex 来一起使用。
当我们希望在堆上分配一个对象供程序的多个部分使用且无法确定哪个部分最后一个结束时,就可以使用 Rc 成为数据值的所有者,例如:
rust
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("hello, world"));
let b = Rc::clone(&a);//仅仅复制了智能指针并增加了引用计数,并没有克隆底层数据
assert_eq!(2, Rc::strong_count(&a));
assert_eq!(Rc::strong_count(&a), Rc::strong_count(&b))
}
Rc<T>是指向底层数据的不可变的引用,因此无法通过它来修改数据,这也符合 Rust 的借用规则:要么存在多个不可变借用,要么只能存在一个可变借用。
Weak弱引用
- 可访问,但没有所有权,不增加引用计数,因此不会影响被引用值的释放回收。
- 可由 Rc<T> 调用 downgrade 方法转换成 Weak<T>。
- Weak<T> 可使用 upgrade 方法转换成 Option<Rc<T>>,如果资源已经被释放,则 Option 的值是 None。
- 常用于解决循环引用的问题。
Cell与RefCell
Rust 提供了 Cell 和 RefCell 用于内部可变性,简而言之,可以在拥有不可变引用的同时修改目标数据,对于正常的代码实现来说,这个是不可能做到的(要么一个可变借用,要么多个不可变借用)。
Cell 和 RefCell 在功能上没有区别,区别在于 Cell<T> 适用于 T 实现 Copy
(一般来说,实现了copy意味着无需使用可变引用)的情况:
rust
use std::cell::Cell;
fn main() {
let c = Cell::new("asdf"); //"asdf"是&str类型,实现了copy特征,如果这里是String就会报错
let one = c.get(); //get取值
c.set("qwer");//set设置值
let two = c.get();
println!("{},{}", one, two);
}
RefCell可以用来解决可变引用和不可变引用共存
的问题,但是没有从根本上解决这种问题,只是将报错从编译期间推迟到了运行时,并从编译错误变成了panic异常。 RefCell主要用于你确信代码是正确的,而编译器却发生了误判时。
Cell和ReCell比较:
- 与 Cell 用于可 Copy 的值不同,RefCell 用于可变引用
- RefCell 只是将借用规则从编译期推迟到程序运行期,并不能绕过这个规则
- RefCell 适用于编译期误报或者一个引用被在多处代码使用、修改以至于难于管理借用关系时
- 使用 RefCell 时,违背借用规则会导致运行期的 panic
多线程
rust中使用多线程
使用 thread::spawn
可以创建线程:
rust
use std::thread;
fn main() {
let v = vec![1, 2, 3];
let handle = thread::spawn(move|| { //线程中无法直接借用外部环境中的变量值,但可以使用move关键字将v的所有权转移到线程中去
println!("Here's a vector: {:?}", v);
});
handle.join().unwrap();//等待子线程结束
}
线程屏障,使用Barrier让多个线程都执行到某个点后,才继续一起往后执行:
rust
use std::sync::{Arc, Barrier};
use std::thread;
fn main() {
let mut handles = Vec::with_capacity(6);
let barrier = Arc::new(Barrier::new(6));
for _ in 0..6 {
let b = barrier.clone();
handles.push(thread::spawn(move|| {
println!("before wait");
b.wait();//6个线程都打印完before wait后才会一起往后执行
println!("after wait");
}));
}
for handle in handles {
handle.join().unwrap();
}
}
线程局部变量:thread_local!
是一个宏(macro),用于创建线程局部(thread-local)变量。线程局部变量是一种特殊类型的变量,每个线程都有其自己的副本,线程之间的变量互相独立。
线程中只能被调用一次的函数:std::sync::Once中的call_once方法。
线程消息传递
消息通道,标准库提供了通道std::sync::mpsc,该通道支持多个发送者,但是只支持唯一的接收者:
rust
use std::sync::mpsc;
use std::thread;
fn main() {
// 创建一个消息通道, 返回一个元组:(发送者,接收者)
let (tx, rx) = mpsc::channel();
// 创建线程,并发送消息
thread::spawn(move || {
// 发送一个数字1, send方法返回Result<T,E>,通过unwrap进行快速错误处理
tx.send(1).unwrap();
// 下面代码将报错,因为编译器自动推导出通道传递的值是i32类型,那么Option<i32>类型将产生不匹配错误
// tx.send(Some(1)).unwrap()
});
// 在主线程中接收子线程发送的消息并输出
println!("receive {}", rx.recv().unwrap());
}
使用通道来传输数据,一样要遵循 Rust 的所有权规则:
- 若值的类型实现了
Copy
特征,则直接复制一份该值,然后传输过去,例如之前的i32类型。 - 若值没有实现
Copy
,则它的所有权会被转移给接收端,在发送端继续使用该值将报错。
mpsc::channel表示异步通道,发送数据时不会造成阻塞(缓存大小受内存影响);
mpsc::sync_channel(num)表示同步通道,通过num设置缓存大小,发送方发送的数据超过设置的缓存值时会阻塞等待接收方消费数据。
线程同步
互斥锁:
rust
use std::sync::{Arc, Mutex};
use std::thread;
//展示mutex如何使用
fn main() {
// 使用Mutex结构体的关联函数创建新的互斥锁实例
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 || {
// 获取锁,并返回被锁数据的引用(deref特征)
// lock返回的是Result
let mut num = counter.lock().unwrap();
*num += 1;
// 锁自动被drop
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap()); //10
}
内部可变性总结:Rc<T>/RefCell<T>
用于单线程内部可变性, Arc<T>/Mutex<T>
用于多线程内部可变性。
条件变量:
rust
use std::sync::{Arc,Mutex,Condvar};
use std::thread::{spawn,sleep};
use std::time::Duration;
fn main() {
let flag = Arc::new(Mutex::new(false));
let cond = Arc::new(Condvar::new());
let cflag = flag.clone();
let ccond = cond.clone();
let hdl = spawn(move || {
let mut lock = cflag.lock().unwrap();
let mut counter = 0;
while counter < 3 {
while !*lock {
// wait方法会接收一个MutexGuard<'a, T>,且它会自动地暂时释放这个锁,使其他线程可以拿到锁并进行数据更新。
// 同时当前线程在此处会被阻塞,直到被其他地方notify后,它会将原本的MutexGuard<'a, T>还给我们,即重新获取到了锁,同时唤醒了此线程。
lock = ccond.wait(lock).unwrap();
}
*lock = false;
counter += 1;
println!("inner counter: {}", counter);
}
});
let mut counter = 0;
loop {
sleep(Duration::from_millis(1000));
*flag.lock().unwrap() = true;
counter += 1;
if counter > 3 {
break;
}
println!("outside counter: {}", counter);
cond.notify_one();
}
hdl.join().unwrap();
println!("{:?}", flag);
}
信号量:
rust
use tokio::sync::Semaphore;
use std::sync::Arc;
#[tokio::main]
async fn main() {
// 创建一个容量为 2 的信号量
let semaphore = Arc::new(Semaphore::new(2));
// 创建多个任务来获取和释放信号量
let tasks = (0..5).map(|i| {
let semaphore = semaphore.clone();
tokio::spawn(async move {
// 获取信号量许可
let permit = semaphore.acquire().await.unwrap();
println!("Task {} acquired permit", i);
// 模拟任务执行
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
println!("Task {} releasing permit", i);
// 释放信号量许可
drop(permit);
})
});
// 等待所有任务完成
for task in tasks {
task.await.unwrap();
}
}
Atomic原子类型
rust
use std::sync::atomic::{AtomicBool, Ordering};
fn main() {
let flag = AtomicBool::new(true);
// 在多个线程中读取和修改 AtomicBool 值
let thread1 = std::thread::spawn(move || {
println!("Thread 1: Flag = {}", flag.load(Ordering::Relaxed));
flag.store(false, Ordering::Relaxed);
});
let thread2 = std::thread::spawn(move || {
flag.store(false, Ordering::Relaxed);
println!("Thread 2: Flag = {}", flag.load(Ordering::Relaxed));
});
thread1.join().unwrap();
thread2.join().unwrap();
}
Send和Sync特征
Send和Sync是Rust安全并发的重中之重,但是它们只是标记特征(marker trait,该特征未定义任何行为,仅告诉编译器实现了该特征后可以在多线程中安全使用,但真正的安全需要编码者自己把控), 其具体作用是:
- 实现Send的类型可以在线程间安全的传递其所有权。
- 实现Sync的类型可以在线程间安全的共享(通过引用)。
注意:手动实现 Send 和 Sync 是不安全的,通常并不需要手动实现 Send 和 Sync trait,实现者需要使用unsafe
小心维护并发安全保证。
Unsafe Rust
unsafe存在的主要原因是因为 Rust 的静态检查太强了。当遇到一些编译检查是很难绕过的时候,最常用的方法之一就是使用unsafe和pin。使用 unsafe 非常简单,只需要将对应的代码块标记下即可:
rust
fn main() {
let mut num = 5;
let r1 = &num as *const i32;
unsafe {
println!("r1 is: {}", *r1);
}
}
unsafe
能赋予我们 5 种超能力,这些能力在安全的 Rust 代码中是无法获取的:
- 解引用裸指针,就如上例所示。
- 调用一个 unsafe 或外部的函数。
- 访问或修改一个可变的静态变量。
- 实现一个 unsafe 特征。
- 访问 union 中的字段。
裸指针
裸指针(raw pointer,又称原生指针,参考c指针) 在功能上跟引用类似,同时它也需要显式地注明可变性。但是又和引用有所不同,裸指针的声明: *const T
和 *mut T
,它们分别代表了不可变和可变。
裸指针的创建和使用:
rust
fn main() {
//基于引用创建裸指针
let a = 1;
let b: *const i32 = &a as *const i32;
let c: *const i32 = &a; //隐式转换
unsafe {
println!("{}", *c);//*号解引用
}
//基于内存地址创建裸指针
let address = 0x012345usize;
let r = address as *const i32; //不安全
let string = "Hello World!";
let pointer = string.as_ptr() as usize;
let length = string.len();
let (p,len) = from_utf8_unchecked(from_raw_parts(pointer as *const u8, length))
//基于智能指针创建裸指针
let a: Box<i32> = Box::new(10);
let b: *const i32 = &*a;
let c: *const i32 = Box::into_raw(a);//或者使用 into_raw 来创建
}
unsafe函数:
rust
unsafe fn dangerous() {}
fn main() {
unsafe {
dangerous();
}
}
实现unsafe特征:
rust
pub unsafe trait Send {}//send特征
unsafe impl Send for XXX {} //为xxx实现Send特征
宏编程
在 Rust 中宏分为两大类:声明式宏( declarative macros ) macro_rules! 和下面三种过程宏( procedural macros ):
- #[derive],在之前多次见到的派生宏,可以为目标结构体或枚举派生指定的代码,例如Debug特征。
- 类属性宏(Attribute-like macro),用于为目标添加自定义的属性。
- 类函数宏(Function-like macro),看上去就像是函数调用。
声明式宏
声明式宏(macro_rules!)允许我们写出类似 match 的代码。match 表达式是一个控制结构,其接收一个表达式,然后将表达式的结果与多个模式进行匹配,一旦匹配了某个模式,则该模式相关联的代码将被执行。而宏也是将一个值跟对应的模式进行匹配,且该模式会与特定的代码相关联。但是与 match 不同的是,宏里的值是一段 Rust 源代码(字面量),模式用于跟这段源代码的结构相比较,一旦匹配,传入宏的那段源代码将被模式关联的代码所替换,最终实现宏展开。
以动态数组Vector为例,一个简化的实现 vec!:
rust
#[macro_export]
macro_rules! vec {
( $( $x:expr ),* ) => { //vec只有一个模式( $( $x:expr ),* )
{
let mut temp_vec = Vec::new();
$(
temp_vec.push($x);
)*
temp_vec
}
};
}
过程宏
从形式上来看,过程宏跟函数较为相像,但过程宏是使用源代码作为输入参数,基于代码进行一系列操作后,再输出一段全新的代码。注意,过程宏中的 derive 宏输出的代码并不会替换之前的代码,这一点与声明宏有很大的不同。
-
定义一个derive过程宏
rust//有一个特征 HelloMacro,使用过程宏来统一实现该特征,这样只需要对类型进行标记即可实现该特征:#[derive(HelloMacro)] pub trait HelloMacro { fn hello_macro(); } //在特定的文件中定义过程宏 extern crate proc_macro; use proc_macro::TokenStream; use quote::quote; use syn; use syn::DeriveInput; #[proc_macro_derive(HelloMacro)] pub fn hello_macro_derive(input: TokenStream) -> TokenStream { // 基于 input 构建 AST 语法树 let ast:DeriveInput = syn::parse(input).unwrap(); // 构建特征实现代码 let name = &ast.ident; let gen = quote! { impl HelloMacro for #name { fn hello_macro() { println!("Hello, Macro! My name is {}!", stringify!(#name)); } } }; gen.into() } //在项目中使用过程宏 use hello_macro::HelloMacro; use hello_macro_derive::HelloMacro; #[derive(HelloMacro)] struct Sunfei; #[derive(HelloMacro)] struct Sunface; fn main() { Sunfei::hello_macro(); Sunface::hello_macro(); }
-
类属性宏
类属性过程宏跟 derive 宏类似,但是前者允许我们定义自己的属性。除此之外,derive 只能用于结构体和枚举,而类属性宏可以用于其它类型项,例如函数。
rust//假设我们在开发一个 web 框架,当用户通过 HTTP GET 请求访问 / 根路径时,使用 index 函数为其提供服务: #[route(GET, "/")] fn index() {} //这里的 #[route] 属性就是一个过程宏,它的定义函数大概如下 #[proc_macro_attribute] pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {} //与derive宏不同,类属性宏的定义函数有两个参数: //1.第一个参数时用于说明属性包含的内容:Get, "/" 部分 //2.第二个是属性所标注的类型项,在这里是 fn index() {...},注意,函数体也被包含其中
-
类函数宏
类函数宏可以让我们定义像函数那样调用的宏,从这个角度来看,它跟声明宏 macro_rules 较为类似。区别在于,macro_rules 的定义形式与 match 匹配非常相像,而类函数宏的定义形式则类似于之前讲过的两种过程宏:
rust//定义 #[proc_macro] pub fn sql(input: TokenStream) -> TokenStream {} //调用 let sql = sql!(SELECT * FROM posts WHERE id=1); //为何我们不使用声明宏 macro_rules 来定义呢?原因是这里需要对 SQL 语句进行解析并检查其正确性,这个复杂的过程是 macro_rules 难以对付的,而过程宏相比起来就会灵活的多。
异步编程
Rust 选择的异步编程模型是async。它通过语言特性+标准库+三方库实现,优点是支持高并发、以及处理异步IO。与其他语言相比,rust的async有如下特点:
- Future 在 Rust 中是惰性的,只有在被轮询(poll)时才会运行, 因此丢弃一个Future会阻止它未来再被运行,可以将Future理解为一个在未来某个时间点被调度执行的任务。
- Async 在 Rust 中使用开销是零。
- Rust 没有内置异步调用所必需的运行时,需要借助Rust 社区生态提供的运行时实现,例如tokio。
async/await
async/.await是 Rust 内置的语言特性,可以让我们用同步的方式去编写异步的代码。
通过 async 标记的语法块会被转换成实现了Future特征的状态机。 与同步调用阻塞当前线程不同,当Future执行并遇到阻塞时,它会让出当前线程的控制权,这样其它的Future就可以在该线程中运行,这种方式完全不会导致当前线程的阻塞。
使用 async fn 语法来创建一个异步函数:
rust
async fn do_something() {
println!("go go go !");
}
fn main() {
let future = do_something();//注意:异步函数的返回值是一个 Future,若直接调用该函数,不会输出任何结果,因为 Future 还未被执行
block_on(future); //block_on会阻塞当前线程直到指定的Future执行完成
do_something().await;//或者是使用.await使Future执行
}
block_on和.await的区别:block_on会阻塞当前线程,而.await并不会阻塞当前的线程。在async fn函数中使用.await可以等待当前Future完成,在等待的过程中,该线程还可以继续执行其它Future,即await可以保证在当前函数中的同步
,但是和线程中其他Future是异步执行。
rust
use futures::executor::block_on;
struct Song {
}
async fn learn_song() -> Song {
Song {
}
}
async fn sing_song(song: Song) {
}
async fn dance() {
}
async fn learn_and_sing() {
// 这里使用.await来等待学歌的完成,但是并不会阻塞当前线程,该线程在学歌的任务过程中,完全可以去执行跳舞的任务
let song = learn_song().await;
// 唱歌必须要在学歌之后,await保证了当前函数内的同步
sing_song(song).await;
}
async fn async_main() {
let f1 = learn_and_sing();
let f2 = dance();
//join!可以并发的处理和等待多个Future,若learn_and_sing Future被阻塞,那dance Future可以拿过线程的所有权继续执行。若dance也变成阻塞状态,那learn_and_sing又可以再次拿回线程所有权,继续执行。
// 若两个都被阻塞,那么async main会变成阻塞状态,然后让出线程所有权,并将其交给main函数中的block_on执行器
futures::join!(f1, f2);
}
fn main() {
block_on(async_main());
}
Futrue
Futrue,可以理解为未来某个时间点会执行的任务。 Future 特征是 Rust 异步编程的核心,毕竟异步函数是异步编程的核心,而 Future 恰恰是异步函数的返回值和被执行的关键。
一个简化版的Future特征:
rust
trait SimpleFuture {
type Output;
//Future需要被执行器poll(轮询)后才能运行,通过调用该方法,可以推进 Future 的进一步执行,直到被完成为止
//Future 并不能保证在一次 poll 中就被执行完
fn poll(&mut self, wake: fn()) -> Poll<Self::Output>;
}
//若在当前 poll 中, Future 可以被完成,则会返回 Poll::Ready(result) ,反之则返回 Poll::Pending, 并且安排一个 wake 函数:当未来 Future 准备好进一步执行时, //该函数会被调用,然后管理该 Future 的执行器会再次调用 poll 方法,此时 Future 就可以继续执行了。
enum Poll<T> {
Ready(T),
Pending,
}
当使用关键字async修饰函数时,函数会返回一个Futrue。该函数就成为了一个异步函数(有特征的状态机)。值得注意的是,同步函数中不能调用异步函数,如果调用了异步函数,那么同步函数就成了异步函数了。
理解:加了async相当于把函数封装成一个Futrue任务,执行器可以将这些Futrue放到一个任务队列中,轮询获取队列中的任务,依次调用任务的poll函数。如果本次调用结束,则此task运行完成;如果本次调用没有结束,则为task注册wake函数,待task下次可以启动时让其自己调用wake将自己放进任务队列中,供执行器再次执行。
Pin
在 Rust 中,所有的类型可以分为两类:
-
类型的值可以在内存中安全地被移动,例如数值、字符串、布尔值、结构体、枚举,几乎所有类型都可以落入到此范畴内。
-
自引用类型(值在内存中移动不安全):
ruststruct SelfRef { value: String, pointer_to_value: *mut String, //指向value的指针 }
自引用类型容易遇到一个问题,若上述value使用新的字符串赋值,而pointer_to_value依然认为指向之前的字符串,一个重大 bug 就出现了。Pin可以防止这种情况出现,它的作用就是可以防止一个类型在内存中被移动(可以理解为当类型的值不可在内存移动时,通过某种方式安全获取它的所有权和可变引用(如下Pin声明,通过一个指针做到这点),并且获得的值无法在不安全的函数中使用
,例如,std::mem::swap)。
Pin是一个结构体类型:
rust
pub struct Pin<P> {
pointer: P,
}
与Pin相对的是特征UnPin
,绝大多数类型都不在意是否被移动,它们都自动实现了Unpin特征。Unpin是一种标记特征( marker trait ),该特征未定义任何行为,一旦类型定义了Unpin特征就表示其可以在内存中安全移动(实例可以在内存中进行移动,而不会触发 Rust 的默认行为,即在移动值时需要显式地调用 std::mem::drop
来释放旧值)。
使用Pin的总结:
-
若 T: Unpin ( Rust 类型的默认实现),那么 Pin<T> 跟 &mut T 完全相同,也就是 Pin 将没有任何效果, 该移动还是照常移动。
-
绝大多数标准库类型都实现了 Unpin,其中一个例外就是:async/await 生成的 Future 没有实现 Unpin。
-
可以将值固定到栈上,也可以固定到堆上
- 将 !Unpin 值固定到栈上需要使用 unsafe。
- 将 !Unpin 值固定到堆上无需 unsafe ,可以通过 Box::pin 来简单的实现。
-
当固定类型 T: !Unpin 时,你需要保证数据从被固定到被 drop 这段时期内,其内存不会变得非法或者被重用。
同时运行多个Future
-
join!宏,它允许我们同时等待多个不同 Future 的完成,且可以并发地运行这些 Future。
rustuse futures::join; async fn enjoy_book_and_music() -> (Book, Music) { let book_fut = enjoy_book(); let music_fut = enjoy_music(); join!(book_fut, music_fut) //join! 会返回一个元组,里面的值是对应的 Future 执行结束后输出的值。 }
-
try_join!,某一个 Future 报错后就立即停止所有 Future 的执行,特别是当Future返回Result时。
rustuse futures::try_join; async fn get_book() -> Result<Book, String> { /* ... */ Ok(Book) } async fn get_music() -> Result<Music, String> { /* ... */ Ok(Music) } async fn get_book_and_music() -> Result<(Book, Music), String> { let book_fut = get_book(); let music_fut = get_music(); try_join!(book_fut, music_fut)//传给 try_join! 的所有 Future 都必须拥有相同的错误类型 }
-
select!宏,同时等待多个Future,且任何一个Future结束后,都可以立即被处理。相比之下,join! 只有等所有 Future 结束后,才能集中处理结果。
rustuse futures::{ future::FutureExt, // for `.fuse()` pin_mut, select, }; async fn task_one() { /* ... */ } async fn task_two() { /* ... */ } async fn race_tasks() { let t1 = task_one().fuse();//让Future实现FusedFuture特征(当Future一旦完成后,那select就不能再对其进行轮询使用),是使用select必须的 let t2 = task_two().fuse(); pin_mut!(t1, t2);//为Future实现Unpin特征,是使用select必须的 loop //select一般会配合loop使用,否则其中一个任务完成,函数就结束了且不会等待另一个任务的完成 { select! { () = t1 => println!("任务1完成"), () = t2 => println!("任务2完成"), complete => break, //当所有的 Future完成后才会被执行 default => panic!(), //若没有任何Future处于Ready状态,则该分支会被立即执行(和complete类似) } } }
-