关注「Rust编程指北」,一起学习 Rust,给未来投资
五种我认为值得掌握的现代编程语言:
我刻意剔除了三种大语言(仅在本文语境下讨论,不限实际需求考虑):
你认同么?我认同,并且我认为学校教了 C 语言之后,可以直接教 Rust(TODO: 这里有一些支撑的理由,可以再讨论)。
我也直接剔除了各种函数式语言:
函数式语言的的一些范式一直被融入到主流语言里面,日常开发也几乎用不到函数式语言,在函数式语言里面投入时间,边际收益并不高,但你可以花一个暑假沉浸进去认真感受一次,这样就够了。
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
https://play.rust-lang.org/
file to exclude
可以配上**/lib*.json,
,在查找的时候忽略 Rust 自己生成的配置文件上图是 Rust 典型项目文件系统和对应的模块系统,解释如下:
Rust 项目根目录声明和导出模块
[dependencies]
里指定my_project={path="../my_project"}
即可。mod config;
mod manager;
mod objects;
mod util;
mod config;
mod manager;
mod objects; // 含有mod.rs的子目录是一个子模块
mod util; // 含有mod.rs的子目录是一个子模块
pub use manager::*; // 指定全导出
pub use objects::AnyObject; // 指定导出objects模块内的AnyObject
项目子目录声明和导出模块
mod path_util;
声明子模块pub use path_util::*;
导出 path_util 模块内的所有可导出符号使用其他模块
Rust 项目根目录的顶级模块名为crate
Rust 项目根目录下的一级模块是crate::xxxx
,因此引用时应该写use crate::config::Config;
Rust 的子目录下,例如 path_util 里引用本级 cmd_util 有两种方式
use crate::util::CmdUtil
从顶级模块crate
开始指定路径use super::CmdUtil
指定。这是因为path_util
和cmd_util
在模块层级中的同级,可以通过super
来表示上一级模块如果 cmd_util 里的 CmdUtil 是 pub 的,并且有导出(例如 util/mod.rs 里
pub use cmd_util::CmdUtil;
),那么 path_util 里
Linuar Type: https://en.wikipedia.org/wiki/Substructural_type_system
Linear types corresponds to linear logic and ensures that objects are used exactly once, allowing the system to safely deallocate an object after its use.
下面是几个正交的维度 from : https://www.reddit.com/r/rust/comments/idwlqu/rust_memory_container_cheatsheet_publish_on_github/
Internal sharing? -[no]--> Allocates? -[no]--> Internal mutability? -[no]--> Ownership? -[no]-----------------------------------> &mut T
\ \ \ `-[yes]----------------------------------> T
\ \ \
\ \ `-[yes]-> Thread-safe? -[no]--> Internal references? -[no]---> Cell<T>
\ \ \ `-[yes]--> RefCell<T>
\ \ \
\ \ `-[yes]-> Internal references? -[no]---> AtomicT
\ \ \ `-[one]--> Mutex<T>
\ \ `--[many]-> RwLock<T>
\ \
\ `-[yes]------------------------------------------------------------------------------------> Box<T>
\
`-[yes]-> Allocates? -[no]-------------------------------------------------------------------------------------> &T
\
`-[yes]-> Thread-safe? -[no]---------------------------------------------------------------> Rc<T>
`-[yes]--------------------------------------------------------------> Arc<T>
C++从 C 继承而来,对象生命周期的核心问题是:
先看下对象的生命周期:
再看下对象的状态管理:
单线程
不可变对象:可安全使用
可变对象:对象状态需要被
封装
才能处于尽量可控
多线程
非原子
修改状态,A 线程修改了一半,B 对修改了一半的脏数据
进行读写。Rust 引入了一个核心的语义:所有权(Owner),每个对象都有明确的所有权,所有权可以发生两种变化,下面是核心规则:
移动(move
)
,例如
let x=String::from("test"); let y =x;
,赋值语句
let y=x;
将
x
的所有权移动给
y
,则
x
不再可用
需要注意的是,并不是赋值语句都发生了所有权的移动
built in
) 会执行按位拷贝,例如let x = 6; let y = x;
Copy
这个trait
的类型,会进行深拷贝可以看到在
Rust 里拷贝不是默认的,为了拷贝需要付出代价,这是根本性的设计和范式差异
:
trait Copy
,则赋值会自动逐 bit 拷贝trait Clone
,则可以调用xx.clone()
获得副本借用(borrow
)
,将对象的所有权临时借给其他对象,借完要还的!借用又分成两种
【1】不可变借用(immutable borrow
):Rust 允许一个变量同时有多个不可变借用,例如let x=String::from("test"); let y = &x; let z=&x;
,则y
和z
都是x
的不可变借用
【2】可变借用(
mutable borrow
):
Rust 只允许一个变量同时有一个可变借用
,例如
let x=vec![0;32]; let y=& mut x; let z=&mut x; y.push(0);
这里 y 和 z 都发生了对 x 的可变借用,编译器会报错。
- 请在单线程限定下思考这样设计解决了什么问题?
Internal mutability
)有时候,我们需要【不可变借用的内部成员变量可变,在 Rust 里面叫做内部可变性(Internal mutability
)】。那么,有如下选择,它们内部都依赖底层的UnsafeCell
实现,顾名思义这么做是unsafe
的,但是编译器知道这些调用的地方需要特殊处理。
T
实现了 trait Copy
,那么可以使用Cell<T>
RefCell<T>
Mutex<T>
RwLock<T>
对于实现了 Copy 的类型,可以使用 Cell<T>
,官方例子:https://doc.rust-lang.org/std/cell/struct.Cell.html
T
实现了Copy
,则可以调用get
方法,获得 T 的一份逐 bit 拷贝set
方法update
设置并返回新值replace
方法get_mut
方法获得 Cell 变量的可变借用,该方法继续遵循借用规则【1】【2】冲突原则。改造下官方例子,官方例子里只改变了一次不可变借用的 Cell 成员,稍加改造可以多次修改:
use std::cell::Cell;
struct SomeStruct {
regular_field: u8,
special_field: Cell<u8>,
}
fn main() {
let my_struct = SomeStruct {
regular_field: 0,
special_field: Cell::new(1),
};
// 第1次不可变借用
let x = &my_struct;
// 修改1
x.special_field.set(11);
println!("{}", x.special_field.get());
// 第2次不可变借用
let y = &my_struct;
// 修改2
y.special_field.set(3);
println!("{}", x.special_field.get());
// 修改3
x.special_field.set(10);
println!("{}", x.special_field.get());
}
对于没有实现Copy
的类型,例如String
和Vec<T>
,要实现多个不可变借用内部成员的可变性,就需要使用RefCell<T>
,常用方法主要是
borrow()
方法borrow_mut()
方法虽然获得了对不可变借用内部成员的可变修改能力,但是借用的规则【1】【2】依然起作用,下面是一组单元测试,注意 RefCell 的借用规则在编译期不会检查,但是运行期会检查,如果违反会在运行期 panic。
测试 1:x 一旦 borrow_mut,就不可同时 borrow,借用规则【2】
fn test1(){
let x = RefCell::new(5);
let a = x.borrow();
let b = x.borrow_mut(); // 运行期 panic
}
测试 2:x 的 borrow 可多次,借用规则【1】
fn test2(){
let x = RefCell::new(5);
let a = x.borrow();
let b = x.borrow();
}
测试 3:y 是 x 的 clone,x 和 y 都可多次 borrow,遵循借用规则【1】
fn test3(){
let x = RefCell::new(5);
let a = x.borrow();
let b = x.borrow();
let y = x.clone();
let c = y.borrow();
let d = y.borrow();
}
测试 4:y 是 x 的 clone,x 和 y 一起,只能有一个 borrow_mut,借用规则【2】
fn test4(){
let x = RefCell::new(5);
let a = x.borrow_mut();
let y = x.clone();
let c = y.borrow_mut();// 运行期 panic
}
测试 5:y 是 x 的 clone,x 和 y 一起,可多次 borrow,借用规则【1】
fn test5(){
let x = RefCell::new(5);
let a = x.borrow();
let y = x.clone();
let c = y.borrow_mut();
}
测试 6:y 是 x 的 clone,x 和 y 一起,只能有一个 borrow_mut,借用规则【2】,可变借用在超出作用域后归还,即可再次可变借用
fn test6(){
let x = RefCell::new(5);
let y = x.clone();
{
let a = x.borrow_mut();
}
let c = y.borrow_mut();
}
无论是 Cell 还是 RefCell,都是单线程语义下达到内部可变性的能力。在多线程情况下,同样存在一个【不可变借用的内部成员变量可变】的需求。此时,就需要加锁,Rust 的 Mutext/RwLock 不但实现了锁的能力,同时提供了内部可变性的能力。
use std::task;
use std::sync::{Mutex, RwLock}
struct Test{
x: u32
}
// 使用Arc涉及到 内部共享(`Internal sharing`),参考后面
let v = Arc::new(Mutex::new(Test{x:10}))
let v1 = v.clone();
task::spawn(async move {
// 解锁+获得不可变借用
let v = v1.lock().unwrap();
});
let v2 = v.clone();
task::spawn(async move {
// 解锁+获得可变借用
let mut v = v1.lock().unwrap();
});
Allocate
)Rust 的内存分配有三个区域
对象在跨线程间使用
【1】一个对象可以从线程 A 传递给线程 B,此时需要对象类型实现 Send trait
【2】一个对象的借用可以从线程 A 传递给线程 B,此时需要对象类型实现
Sync Trait
T
实现了 Sync
,则 & T
自动实现了Send
=> & T
可以从线程 A 传递给线程 B根据上面的规则【1】,实际上一个对象从线程 A 传递给线程 B 有如下情况
Send
, 也不实现 Sync
Rc
即不实现 Send
也不实现 Sync
,这是因为 Rc
的引用计数并没有使用 Lock 或者 Aotomic,因此不能在多个线程间同时修改引用计数,不能在线程间 Send,更不能 Sync 了Arc
实现了线程安全的引用计数,实现了Send
,如果内部包含的类型可以Sync
,则Arc<T>
也能 Sync
UnsafeCell
没有实现Sync
,因此 Cell
和 RefCell
也没有实现 Sync
,但是可以Send
Internal sharing
)Rust 的有所有权唯一原则,但是有些时候,我们需要在多处持有一个不可变对象的所有权,这叫做内部共享(Internal sharing
)有两种情况
单线程:
多线程:
上面几个小节都是 Rust 的所有权问题,本节讨论 Rust 里独立的借用对象的生命周期标识符。
一、函数参数上的 lifetime 标记:(1)首先,Rust 的编译器需要明确地知道一个借用对象是否还是有效的。例如返回一个新创建的对象肯定是有效的,不需要检查。
fn create_obj():Object{
Object{}
}
(2)但是,显然你不能返回一个局部对象的借用,因为局部对象在函数结束后超出作用域就被释放了:
fn get_obj():&Object{ // compile error
const obj = Object{};
&obj
}
(3)不过,如果这个借用本来就是从外部传入的,那当然可以返回,函数结束后这个对象还是有效的:
// I am borrowed from caller
// return borrow to the caller is safe
fn process_obj(obj:&Object):&Object{
&obj
}
(4)然而,如果你传入了两个对象的借用,内部做了条件返回。那么编译器没那么智能,它并不总是能推断出返回的是哪个对象的借用:
// compile error!
// where am I come from?
fn process_objs(x:&Obejct, y:&Object):&Object{
if(x.is_ok()){
&x
}else{
&y
}
}
(5)因此,Rust 保留了内部的一种编译器内部的,本来是隐式添加记号,也就是生命周期(lifetime),通过显式添加生命周期标记,解决上述问题:
// I am come from 'a lifetime, NOT 'b
fn process_objs<'a,'b>(x: &'a Obejct, y:&'b Object):&'a Object{
&x
}
// I am come from 'a lifetime, x,y,and result are all 'a lifetime
fn process_objs<'a>(x: &'a Obejct, y:&'a Object):&'a Object{
if(x.is_ok()){
&x
}else{
&y
}
}
(6)事实上,当你没写 lifetime 标记时,每个对象也都是有对应的 lifetime 的,例如编译器为每个对象生成一个不同的 lifetime
fn test<'a,'b>(x: &'a Obejct, y:&'b Object){
}
(7)因为默认生成的都是不同的,所以返回值如果不标记是谁,编译器就无法推断:
fn test<'a,'b,'c>(x: &'a Obejct, y:&'b Object):&'c Object{ // 'c is 'a or 'b ?
if(x.is_ok()){
&x
}else{
&y
}
}
(8)所以如果我们显式标记,并让两个变量用同一个,就能解决,这就是告诉编译器,'c='a='b
:
fn test<'a>(x: &'a Obejct, y:&'a Object):&'a Object{ // 'c='a='b, they are all 'a
if(x.is_ok()){
&x
}else{
&y
}
}
(9)看到这里,你也应该知道了 lifetime 标记的名字是任意的,只是一个【形参】,代表的是这个借用对象的生命周期作用域的名字:
{
let obj; //---'a start here
{
let x = Obj{}; //---'b start here
obj = &x; //---'b finish here
}
println!("obj: {}", obj); //---'a finish here, 'b is out of scope, compile error!
}
// #[derive(Apparition)]
{
// 当然你可以用任意合法的符号替换'a和'b,它们只是个名字
let obj<'b>; //---'a start here
{
let x = Obj{}; //---'b start here
obj<'b> = &'b x; //---'b finish here
}
// obj借用是否有效,仅仅取决于它实际上它所借用的对象的生命周期作用域'b范围是否大于等于'a
// 一个'b作用域内的对象的借用,在'a内被调用,但是'b比'a小,调用的时候'b已经不存在了
// 因此编译器宣布:这是非法的。
println!("obj: {}", obj<'b>); //---'a finish here, 'b is out of scope, compile error!
}
二、结构体成员的 lifetime 标记:在 Rust 里面一个结构体的成员变量如果是一个外部对象的借用,那么必须标识这个借用对象的生命周期
struct Piece<'a>{
slice:&'a [u8] // 表明slice是来自外部对象的一个借用,'a只是一个生命周期形参
}
// Piece的定义里面,'a 表示vec的生命周期,
// 下面的例子调用,vec的生命周期至少应该大于等于piece的生命周期
// 简单说vec存活的作用域应该大于等于piece的存活作用域
fn test(){
let vec = Vec::<u8>::new();
let piece = Piece{slice: vec.as_slice()};
}
// 下面就是错的, piece返回后,vec已经挂了
// 不满足vec的生命周期大于等于piece的生命周期这条
fn test_2()->Piece{
let vec = Vec::<u8>::new();
let piece = Piece{slice: vec.as_slice()};
piece // compile error: ^^^^^ returns a value referencing data owned by the current function
}
如果有两个不同的成员,分别持有外部对象的借用,那么他们应该使用一个生命周期标识还是两个呢?
struct Piece<'a>{
slice_1: &'a [u8], // 使用相同的生命周期标识
slice_1: &'a [u8], //
}
// Piece的定义里面,'a只是表示slice_1和slice_2所借用的对象的存活范围在一个相同的作用域内,
// 而不是说slice_1和slice_2所借用的对象必须是同一个,区分这点很重要
fn test_1(){
// slice_1 和 slice_2 借用了同一个对象vec
let vec = Vec::<u8>::new();
let piece = Piece{slice_1: vec.as_slice(), slice_2: vec.as_slice()};
}
fn test_2(){
// slice_1 和 slice_2 借用了两个不同的对象
let vec_1 = Vec::<u8>::new();
let vec_2 = Vec::<u8>::new();
let piece = Piece{slice_1: vec_1.as_slice(), slice_2: vec_2.as_slice()};
}
// 如果所借用的两个对象的存活返回不同,'a只会取他们生命周期的最小的交集
// 下面这个例子,'a 和 vec_1的作用域相同
fn test_3(vec_2:&Vec<u8>){
// slice_1 和 slice_2 借用了两个不同的对象
let vec_1 = Vec::<u8>::new();
let piece = Piece{slice_1: vec_1.as_slice(), slice_2: vec_2.as_slice()};
}
// 因此,如果把piece返回就会出错,因为piece的生命周期不能超过vec_1
fn test_4(vec_2:&Vec<u8>)->Piece{
// slice_1 和 slice_2 借用了两个不同的对象
let vec_1 = Vec::<u8>::new();
let piece = Piece{slice_1: vec_1.as_slice(), slice_2: vec_2.as_slice()};
piece
// compile error: ^^^^^ returns a value referencing data owned by the current function
}
// 显然,稍加改造就可以:
fn test_5<'a>(vec_1:&'a Vec<u8>, vec_2:&'a Vec<u8>)->Piece<'a>{
// slice_1 和 slice_2 借用了两个不同的对象
let piece = Piece{slice_1: vec_1.as_slice(), slice_2: vec_2.as_slice()};
piece
}
三、结构体成员函数的 lifetime 标记:
结构体成员函数和普通函数一样,可以有生命周期标识
struct Range{
start: usize,
len: usize
}
impl Range{
// 接受一个外部的Vec对象的借用作为参数
// 返回这个Vec的片段的一个借用
// 因此,需要引入生命周期标识
// 表明返回的&[u8]的生命周期和传入的owner的生命周期一致
pub fn as_slice<'a>(&self, owner: &'a Vec<u8>)->&'a [u8] {
let slice = &owner[self.start..self.end()];
slice
}
}
下面的代码会出错:
enum AdvancedPiece{
Range(Range),
Vec(Vec<u8>)
}
impl AdvancedPiece{
pub fn as_slice<'a>(&self, owner: & 'a Vec<u8>)->&'a [u8] {
match self {
AdvancedPiece::Range(range)=>{
range.as_slice(owner) // range.as_slice(owner)返回的&[u8]生命周期和owner一致,用'a标记
},
AdvancedPiece::Vec(vec)=> {
&vec // compile error: &vec的生命周期和owner并不一致
}
}
}
}
结构体生命周期标识的一个需要注意的地方是,&self 也是可以标注生命周期的,因为&self 本身也是一个借用,既然是借用,就可以标记生命周期。从这个角度也可以进一步理解,生命周期就是标记借用对象的存活作用域用的。上述代码,实际上等价于:
enum AdvancedPiece{
Range(Range),
Vec(Vec<u8>)
}
impl AdvancedPiece{
// self有自己独立的生命周期,用独立的生命周期标识'b 标记出来
// 这样就看得更清楚了
pub fn as_slice<'a,'b>(&'b self, owner: & 'a Vec<u8>)->&'a [u8] {
match self {
AdvancedPiece::Range(range)=>{
range.as_slice(owner) // range.as_slice(owner)返回的&[u8]生命周期和owner一致,用'a标记
},
AdvancedPiece::Vec(vec)=> {
&vec // compile error: &vec的生命周期是'b , 返回值需要的是'a
}
}
}
}
因此,我们可以标记&self 和 owner 的生命周期是一致的来向编译器说明需求:
enum AdvancedPiece{
Range(Range),
Vec(Vec<u8>)
}
impl AdvancedPiece{
// 约定调用as_slice在self和owner的生命周期交集'a内是合法的
pub fn as_slice<'a>(&'a self, owner: & 'a Vec<u8>)->&'a [u8] {
match self {
AdvancedPiece::Range(range)=>{
range.as_slice(owner) // range.as_slice(owner)返回的&[u8]生命周期和owner一致,用'a标记
},
AdvancedPiece::Vec(vec)=> {
&vec // 此时,&vec的生命周期也是'a
}
}
}
}
四、省略生命周期标识/匿名生命周期标识:
上述代码里面,Rust 在带有生命周期标识的函数或者结构体调用的时候,允许省略显式写生命周期标识,就像泛型参数在编译器可以自动推导类型时可以省略一样:
fn args<T: ToCStr>(&mut self, args: &[T]) -> &mut Command // elided
fn args<'a, 'b, T: ToCStr>(&'a mut self, args: &'b [T]) -> &'a mut Command // expanded
下面是结构体使用中省略生命周期标识的例子
struct Piece<'a>{
slice:&'a [u8] // 表明slice是来自外部对象的一个借用,'a只是一个生命周期形参
}
fn create_piece_1<'a>(vec:&'a Vec<u8>)->Piece<'a>{
Piece{slice:&vec}
}
fn create_piece_2(vec:&Vec<u8>)->Piece{
Piece{slice:&vec}
}
但是,有的时候,我们希望显式表示生命周期,让代码更“清晰”,可以用匿名生命周期
fn create_piece_3(vec:&Vec<u8>)->Piece<'_>{ // '_ 标记返回值Piece的生命周期参数,但是不必在函数和参数里面标记生命周期
Piece{slice:&vec}
}
同样的,结构体的 impl 里也可以用匿名生命周期简化代码:
impl<'a> Piece<'a>{
fn create_piece_4(vec:&'a Vec<u8>)->Piece<'a>{
Piece{slice:&vec}
}
}
impl Piece<'_>{
fn create_piece_5(vec:&Vec<u8>)->Piece<'_>{
Piece{slice:&vec}
}
}
五、结构体的一个成员变量借用另一个成员变量的情况:
// TODO(先写一个使用 Buffer/Pieces 的例子)
https://doc.rust-lang.org/book/ch17-01-what-is-oo.html
To many people, polymorphism is synonymous with inheritance. But it’s actually a more general concept that refers to code that can work with data of multiple types. For inheritance, those types are generally subclasses. Rust instead uses generics to abstract over different possible types and trait bounds to impose constraints on what those types must provide. This is sometimes called bounded parametric polymorphism.
其中,传统 OO 里多态是运行时多态,常规的实现是通过继承来达成的:Inheritance as a Type System and as Code Sharing ,但是继承共享代码一般会导致三种问题:
从 C++的模版编程+Concept 概念开始,泛型+萃取这种编译期,通过两种不同的抽象维度来实现多态,叫做:bounded parametric polymorphism.
Rust 在 OO 编程上的选择,采用的正是完备的编译期 OO+多态设计:
通过 struct 抽象数据,语法是 struct Data{}
通过为 struct 提供实现抽象行为,是否pub
用来控制行为的封装,语法是impl Data{ fn method(){} }
通过是否公开数据和行为来控制封装细节,但是一般来说除非一个对象是用来做 POD(Plain Old Data)的纯 Component,一般数据结构的字段不应该暴露:
pub struct Data{ pub no: u32}
impl Data{ pub fn new()->Data{Data{no:0}}
通过泛型抽象不同类型:
fn test<T>(t:T){}
fn test<T> where T: Clone{}
通过 trait 抽象类型必须拥有的能力:* trait Echo{ fn echo();}
* impl Echo for Data{ fn echo(){}}
* fn test<T>(t:T) where T:Echo {}
这里的特点是,你可以为一个 struct 提供不同的 trait,例如:* trait Clone{ fn clone()->Self;}
* trait Echo{ fn echo()->Self;}
* impl Clone for Data{ fn copy()->Self{...}}
* impl Echo for Data{ fn echo(){...} }
这和传统 OOP 为一个 class 提供多个 interface 抽象并不相同 * trait
和 interface
本身都是正交抽象的,一个抽象只做一件事 * trait-struct
是通过外挂
方式提供抽象,而interface-class
是通过继承
方式提供抽象,这意味着当你不引入一个为某个struct
提供的trait
时,你看不到该struct
的外挂,而interface
则是耦合在 class 的实现里。* trait
是编译期多态,interface
是运行期多态
Static Dispatch
)Rust 基于 Trait 实现静态分发,所谓静态分发就是指在编译期实现多态。
情景 1:
trait Echo{
fn echo(&self);
}
struct Test{
}
struct Test2{
}
impl Echo for Test{
fn echo(&self){
}
}
impl Echo for Test2{
fn echo(&self){
}
}
fn do_something(t:&impl Echo){
}
fn get_something(value:bool)->impl Echo{
if value {
Test{}
}else{
Test2{}
}
}
let t = Test{}
do_something(&t);
let v = get_someting(false); // 编译错误
这里的impl Echo
只是一个简写,编译器会确定 t 的具体类型,但是一次调用中类型是唯一确定的,并不能动态切换,因此do_something()
可以正确被静态确定 t 的类型,但是get_something()
编译会出错,因为->impl Echo
并不是说可以返回【任意实现了 Echo 的类型】,而只是一个简写,函数体内必须返回同一种类型。如果需要【任意实现了 Echo 的类型】,应该做成泛型:
fn get_somethig<T:Echo>(value:bool)->T{ //不过使用的地方如果编译器不能推导出T的类型,应该明确指定T的类型
if value {
Test{}
}else{
Test2{}
}
}
大部分时候,静态分发都是和泛型一起使用的:
fn test<T,U>(t:&T)-> where T:Echo+Clone+Debug, U:Echo+Display{
}
这里的Echo+Clone+Debug
属于【Intersect Type】也就是 T 需要同时实现这几个 Trait,泛型和 Trait 的配合是 Rust 静态分发的基本范式。
Dynamic Dispatch
)动态分发,就是和传统 OOP 那样,在运行期才能确定类型,编译器在编译期只能确定其 Trait 类型。但是由于只知道 Trait 信息,无法确定具体类型,就不能确定类型的确定性大小,因此不能在 Stack 上分配对象,需要用 Box 包一层,T 分配在 Heap 上。Box 指针则是确定性大小的,指针本身分配在 Stack 上。又为了避免 Box 的含义的混淆,语法上需要加dyn
关键字:Box,例如
fn test(t: Box<dyn Echo>){
}
参考:[1] https://blog.rust-lang.org/2015/05/11/traits.html
一句话说明 Rust 的闭包:闭包的本质是编译器帮你生成了一个实现(impl)了 Fn/FnMut/FnOnce 等 Trait 的匿名 struct
原子类型
bool
u8/u16/u32/u64/u128
i8/i16/i32/i64/i128
usize
struct XXX{}
结构体类型
enum C{ A(u32), B(String) }
枚举类型 (带 tag 的 Union),配合模式匹配使用
union
联合类型(C 风格无 tag 的 Union),Unsafe 下配合模式匹配使用
tuple, (a,b,c)
unit
类型: ()
, 只有唯一的值()
new type: struct XXX();
let name = MyString(String::new("xxx"))
或者 let name = MyString{0:String::new("xxx")}
使用:println!(name.0)
type MyString=String;
只是制造了一个别名。而使用 new type 则是制造了一个新的独立类型,代价是内部嵌套的类型的方法和属性都必须在新类型上重新导出才可以直接被外部使用,否则就得通过 xxx.0 先获取内部类型再调用。很多时候 new type 可以解决封装问题和孤儿原则问题(TODO:如有必要此处可详细展开)。trait: TypeClass
字符串:
String
, 堆上分配内存&str
,String 的 Slice 类型容器
Vec<T>
,堆上分配内存&[T]
, Vec 的 Slice 类型指针
& T
, &mut T
Cell<T>
, RefCell<T>
, Mutex<T>
, RwLock<T>
Rc<T>
, Arc<T>
Box<T>, Pin<T>
底类型(nerver
): !
枚举类型配合模式匹配使用是最佳搭档
enum Test{ A(i32), B(String) }
let t = Test::A(0);
match t {
Test::A(v)=>{},
Test::B(v)=>{}
}
错误处理可以用if
模式匹配:
fn test()->Result<T,Error>{}
let ret = test();
if let Err(e) = ret {
}
let value = ret.unwrap();
可以用直接模式匹配:
fn test()->Result<T,Error>{}
match test() {
Ok(value)=>{},
Err(e)=>{}
}
但是最常用用的是错误可选的错误类型映射+问号求值,错误处理不再卡壳主线流程:
fn test()->Result<String,Error>{
}
fn other()->Result<String, OtherError>{
let value = test().map_err(|err|{
// 错误类型转换,同类型就不需要转换
Err(OtherError::from(err))
})?; // 问号求值,如果出错就直接返回错误,规避了其他语言的各种if err 处理
// do something...
Ok(value)
}
https://book.async.rs/introduction.html
在当前 Executor 里发起一个异步任务
use async_std::task;
task::spawn(async move {
});
在一个线程里发起异步任务
use async_std::thread;
thread::spawn(move ||{
task::block_on(async { {
});
})
示例的链式异步+错误处理+异步+错误处理...
let v = fetch().await.map_err(|err|{...})?.another_fetch().await.map_err(|err|{...})?;
本质上并不存在【真异步】,所有的异步都是伪装出来的,本质上【异步=独立开一个线程循环轮询】
独立开一个线程,循环轮询操作系统相关的事件,例如 socket,这种轮询方式被叫做 Reactor 模式,每次轮询的时候问下系统是否有新的可用事件(Event)
独立开一个线程,循环轮询一个
Future
,这种轮询是 Executor 做的。
Future
就是提供了一个poll
方法的对象,每次轮询的时候调用一次 poll, 如果状态位 Ready,就结束从队列里移除该 Future,否则继续。await
是组合 Future 的语法糖。一直组合到 main 函数,返回一个顶层的 Future,被反复轮询。async/await
提供了魔法,但是拆开盒子又没有魔法,这是编程的核心乐趣所在。
如何写一个定时器泵:
use async_std::prelude::*;
use async_std::stream;
use std::time::Duration;
let mut interval = stream::interval(Duration::from_secs(4));
while let Some(_) = interval.next().await {
println!("prints every four seconds");
}
如何写一个可调度的定时器泵:
// 创建一个channel
let (cmd_sender, cmd_recver) = async_std::sync::channel(8);
// 异步创建一个泵
async_std::task::spawn(async move {
loop {
let cmd_recver = ctx.cmd_recver.clone();
let cmd = async_std::io::timeout(Duration::from_millis(500), async move {
cmd_recver.recv().await.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))
}).await;
// 此时要么过了500毫秒,要么cmd_recver收到了一个cmd_sender投递的信号
}
});
// 在其他地方调度
cmd_sender.send(());
use log::*;
use simple_logger;
fn main(){
simple_logger::SimpleLogger::new().with_level(LevelFilter::Debug).init().unwrap();
info!("{}",1000);
warn!("{}",1000);
error!("{}",1000);
debug!("{}",1000);
}
pub struct Object{
name: String,
id: Option<u32>,
email: Option<String>
}
impl Object{
pub fn new(name:String)->ObjectBuilder{
ObjectBuilder::new(name)
}
}
pub struct ObjectBuilder{
name: String,
id: Option<u32>,
email: Option<String>
}
impl ObjectBuilder{
pub fn new(name:String)->Self{
// Builder的构造函数只传入必须有的字段
Self{
name,
id:None,
email: None,
}
}
pub fn id(mut self, id:u32>)->Self{
// 设置可选字段,注意self的所有权进来又出去
self.id = Some(id);
self
}
pub fn email(mut self, email:String)->Self{
// 设置可选字段,注意self的所有权进来又出去
self.email = Some(email);
self
}
pub fn build(self)->Object{
// 构造Obejct,注意self的所有权进来,成员都被move给了Object,self所有权结束使用
Object{
name: self.name,
id: self.id,
email: self.email
}
}
}
// 使用
let obj = Object::new(String::from("fanfeilong")).id(13u32).email(String::from("fanfeilong@example.com")).build();
pub struct Object{
name: String,
data: Vec<u8>
}
pub struct ObjectMore{
name: String,
id: Option<u32>,
email: Option<String>
}
impl Object{
// 消耗掉self的所有权,返回成员元组
pub fn split(self)->(String, data){
(self.name, self.data)
}
}
// 消耗掉Object,将其成员Move给ObjectMore,同时对data做进一步的细化转换,保持最小内存分配开销
let obj = Object{..}
let (name, data) = obj.split();
let (id, email) = decode(data);
let obj_more = ObjectMore{name, id, email);
例如有如下的带 lifetime 的 trait
pub trait RawDecode<'de>: Sized {
fn raw_decode(buf: &'de [u8]) -> BuckyResult<(Self, &'de [u8])>;
}
为它扩展一个 trait 时,生命周期会传染到上层,需要外层传入 buf:
pub trait FileDecoder<'de>: Sized {
fn decode_from_file(file: &Path, buf: &'de mut Vec<u8>) -> BuckyResult<(Self, usize)>;
}
impl<'de,D> FileDecoder<'de> for D
where D: RawDecode<'de>,
{
fn decode_from_file(file: &Path, buf: &'de mut Vec<u8>) -> BuckyResult<(Self, usize)> {
match std::fs::File::open(file) {
Ok(mut file) => {
// let mut buf = Vec::<u8>::new();
if let Err(e) = file.read_to_end(buf) {
return Err(BuckyError::from(e));
}
let len = buf.len();
let (obj, buf) = D::raw_decode(buf.as_slice())?;
let size = len - buf.len();
Ok((obj, size))
},
Err(e) => {
Err(BuckyError::from(e))
},
}
}
}
可以通过在 where 字句中使用 for 表达式来阻断生命周期传染,因为我们可以确定 buf 的生命周期在函数内时够用的:
pub trait FileDecoder2: Sized {
fn decode_from_file(file: &Path) -> BuckyResult<(Self, usize)>;
}
impl<D> FileDecoder2 for D
where D: for<'de> RawDecode<'de>,
{
fn decode_from_file(file: &Path) -> BuckyResult<(Self, usize)> {
match std::fs::File::open(file) {
Ok(mut file) => {
let mut buf = Vec::<u8>::new();
if let Err(e) = file.read_to_end(&mut buf) {
return Err(BuckyError::from(e));
}
let len = buf.len();
let (obj, buf) = D::raw_decode(buf.as_slice())?;
let size = len - buf.len();
Ok((obj, size))
},
Err(e) => {
Err(BuckyError::from(e))
},
}
}
}
pub trait DescType{
fn type()->u32;
}
pub trait Object{
type Desc: DescType;
fn type_info()->String{
let type = Desc::type();
type.to_string()
}
}
pub struct RealDescType{
}
impl DescType for RealDescType{
fn type()->{ 0u32 }
}
pub struct RealObject{
}
impl Object for RealObject{
type Desc = RealDescType;
}
// 可以在泛型里使用Object,以及Object关联的Desc类型
pub struct ObjectDescript<O:Object>{
instance: O,
desc: O::Desc, // 则Desc可以跟随O发生变化,这属于编译期多态
}
// 可以根据Desc是否实现了某些Trait来为ObjectDescript自动实现某些Trait
// 例如,如果O::Desc实现了Debug,则自动为ObjectDescript<O>实现Debug
impl Debug for ObjectDescript<O> where O: Object, O::Desc: Debug{
}
泛型成员变量可以达到基于组合来做基类/子类的能力,子类变成了一个需要被组合的泛型类型参数,例如:
pub trait Sub{
type Desc: ObjectDesc;
}
pub struct Base<Content:Sub>{
name: String,
desc: Content::Desc, // 子类通过关联类型来【定制】父类的某些关键成员变量的类型,但是该成员变量的布局是放在父类这里,子类Content本身不需要持有desc。
content: Content // 直接嵌入的子类部分数据
}
这里的父类/子类,只是一个兼容传统 OOP 的说法,实际上这里都是泛型类。
如果 A 和 B 互相依赖
pub struct A{
b: B
}
pub struct B {
a: A
}
拆解出一个共同的部分 C 来消除依赖:
pub struct C {
}
// 让A和B共同依赖C,A和B之间保持线性依赖
pub struct A {
c: C,
b: B
}
// 则A里面需要被B调用的方法只要做成非成员方法即可:
impl A{
pub fn call(c: &C){
}
}
pub struct B {
c: C
}
impl B {
pub fn some(&self){
// B根本不需要持有A,只要有C就可以调用A,或者call直接就是C的方法即可
A::call(&self.c)
}
}
/// ## 定义一个订阅回调Trait
#[async_trait]
pub trait FnSubscriber: Send + Sync + 'static {
async fn call(&self, topic_id: TopicId, device_id:DeviceId) -> BuckyResult<()>;
}
/// ## 自动从Fn转型为FnSubscriber
#[async_trait]
impl<F, Fut> FnSubscriber for F
where
F: Send + Sync + 'static + Fn(TopicId, DeviceId) -> Fut,
Fut: Future<Output = BuckyResult<()>> + Send + 'static,
{
async fn call(&self, topic_id: TopicId, device_id:DeviceId) -> BuckyResult<()> {
let fut = (self)(topic_id, device_id); // 直接调用F:Fn(TopicId, DeviceId)
let res = fut.await?; // 异步等待
Ok(res.into()) // 返回结果
}
}
pub struct Test{
subscribers: Vec<Arc<dyn FnSubscriber>>, //动态分发
}
impl Test{
pub fn new()->Self{
Self{
subscribers: Vec::new(),
}
}
// 注册事件
pub fn on_subscribe(&mut self, callback: impl FnSubscriber){
self.subscribers.push(Arc::new(callback));
}
// 触发事件
async fn emit_subscribe(&self, topic_id: &TopicId, device_id:&DeviceId)->BuckyResult<()>{
for callback in self.subscribers.iter() {
callback.call(topic_id.clone(), device_id.clone()).await?;
}
Ok(())
}
}
首先看下 Arc 和 Mutex 的正确配合:
只应该在T的需要被修改的成员变量上加Mutex
例如:
// 顶层类型是个Arc<T>的封装,使用new type的方式包装一层
// Something可以被安全都在多线程task里clone后传递
struct Something(Arc<Something>);
impl Something(Arc<Something>){
new(y:String,a:String,p:u32)->Self{
return Self{
0:SomethingInner{y,x:Other{a,b:Third{p,q:Mutex::new(Vec::new())}}}
}
}
// TODO:在此添加暴露SomethingInner方法给外部的成员函数,这个重复是必要的
}
struct SomethingInner{
y: String;
x: Other;
}
struct Other{
a: String;
b: Third;
}
struct Thrid{
p: u32;
q: Mutex<Vec<String>>; // 如果只有这个需要修改,只需这里加Mutex
}
impl Third{
fn append(&self, e:String){
self.q.lock().unwrap().push(e); // 通过Mutex的内部可变性来修改q
}
}
其次,我们看下同步锁和异步锁
std::sync::Mutex
async_std::sync::Mutex
async_std::sync::Mutex
实现了Send
接口,因此可以跨越await
点,例如:async fn append(&self, e:String){
// 获取异步锁的Guad对象
let list = self.q.lock().await().unwrap(); // 通过Mutex的内部可变性来修改q
// 异步调用点
// 调度器会可能会在此处返回后下次再次进入到这里继续后面的执行,
// 两次执行可能不在一个线程
waint().await();
// 使用异步锁的Guad对象
// 这里可能和list获取时不在一个线程,因此,list需要实现`Send`
// 同步锁无此能力
list.push(e);
}
但是,上述做法大部分时候时错的。原因在于异步锁改变了锁的作用:
async_std::sync::Mutex
Rust 的孤儿原则导致,如果一个 struct S 和一个自定义 trait T 都不在该项目中,无法使用 T 为 S 添加扩展,也无法为 S 提供新的 impl。因此,可以通过定义一个新的在本项目里的 trait,来为某个不在本项目里的 struct 实现扩展,也可以是为实现了某个 Trait 的泛型提供扩展。
[1] 给 Java/C#/C++程序员的 Rust 介绍,这种教程风格是我最喜欢的,通过 Diff,Step By Step 引入概念设计上的不同:https://fasterthanli.me/articles/i-am-a-java-csharp-c-or-cplusplus-dev-time-to-do-some-rust
[2] 作者学习 Rust 遇到的一个个怪(阻挠,Frustrat),但是作者一步步打怪升级,把概念吃的很透彻:https://fasterthanli.me/articles/frustrated-its-not-you-its-rust
[3] Rust 小抄:https://www.programming-idioms.org/cheatsheet/Rust
[4] Rust 引入了一堆概念,可以看看王垠对 Rust 的设计上的一些问题的评价:http://www.yinwang.org/blog-cn/2016/09/18/rust
[5]why rust is meant to replace c https://evrone.com/rust-vs-c
[6] awesome-rust: rust常用库大全 https://github.com/rust-unofficial/awesome-rust
[7]使用vector-index方式构造graphs数据结构 http://smallcultfollowing.com/babysteps/blog/2015/04/06/modeling-graphs-in-rust-using-vector-indices/
[8]使用rc>方式构造graphs数据结构 https://github.com/nrc/r4cppp/blob/master/graphs/README.md
[9] Rust Async Book,看这个文档就够了:https://rust-lang.github.io/async-book/01_getting_started/01_chapter.html
原文链接:https://www.cnblogs.com/math/p/rust.html 作者 范飞龙
推荐阅读
觉得不错,点个赞吧
扫码关注「Rust编程指北」