本文信息量较大,要求有一定C++开发基础的开发人员才建议阅读。必备知识:
Rc
,然后你发现要多线程,就会把Rc
替换成Arc>
;完了你发现这个东西在析构函数里面需要移出所有权,又得套一层Option
变成Option>>
……要实现的东西越复杂,套的娃就越来越多。trait
之间横向动态变换,这也是一种多态的方式。通过给一个数据结构“插入”trait
,就可以让数据结构通过dyn
关键字支持多种方法,很是奇妙。老生常谈的基本类型:
u8, i8, u16, i16,i32, i64, u32, u64, i128, u128, usize
,其中usize
类似C++的std::size_t
,与平台相关;f32, f64
;true, false
;char
,注意,Rust中的char
不是C/C++中的char
,Rust的char
是完整的Unicode标量,例如 ‘a’ 或者 ‘’。()
元组,[]
切片;元组类似于std::tuple
,切片类似于std::string_view
;除了基本变量,和实现了Clone
“特征”以外的类型,他们的“赋值”(=
号)都是“移动语义”,在后面的所有权章节会详细讨论这个问题。
声明变量的方式:
struct A; // 声明一个占位符类 就是一个不包含任何成员的类
struct B {
field: i32,
} // 声明一个包含了1个有名称的字段的类
struct C(i32, i32); // 声明一个包含了2个i32类型 匿名字段的类 实际上是一个元组
enum Status {
Ok,
Fail,
Other(B)
} // 声明一个枚举类型
const ERR_CODE_OPERATE_FAIL : u32 = 888u32; // 声明一个常量 规范要求常量变量名为 全大写+下划线
// ---- snip ----
let a: i32 = 3; // 显式指定类型
let b = "hello world"; // 自动推导类型 (推导为 &str 切片类型,后面我们再谈)
let c = a; // c获得了a的拷贝 因为a是基本类型 i32
Rust中,注释只有//
开头的单行注释。
注意,struct
的声明体,只有占位符和元组类末尾需要加;
分号,其他的包括enum
都不需要分号。
Rust中的enum
类型类似于C++的enum class
,但是比C++更进一步,枚举值可以是类型,也就是说,可以包含更多信息。
另外,let
声明变量只能在函数体内使用,而const
声明常量可以在全局范围(main
函数体外)以及函数体内使用。
fn main()
函数的.rs
文件开始的。fn main()
函数可以没有返回值,默认返回值为()
,是一个空元组(tuple
);fn main
函数的返回值也可以是类型;;
分号来分隔(和C++一致);return
,或者省略return
和最后的;
分号,那么最后一个表达式将作为函数的值;{}
大括号包裹的多个表达式(这个概念和C++一致);语句块也可以有值,它的值就是最后一个省略掉;
分号的表达式的值;注意,在Rust中,所有流程控制都是不需要使用()
将表达式括起来的,这点和C++有所不同。并且,由于Rust不存在隐式转换,因此,表达式必须是通过逻辑运算符计算得到的布尔类型,或者单纯的true
或者false
。也就是说,不存在“0为假,非0为真”这种说法。
和C++不一样的是,Rust只提供了ranged for循环,如:
for i in (0..10) {
println!("{i}");
}
注意这里的..
运算符,生成了一个可供迭代的数值序列,包含了从[0,10)
的左闭右开区间。
let mut i = 0;
while i < 3 {
println!("hello world");
i += 1;
}
注意,Rust中不存在类似C/C++的前/后++
运算符,只能通过+=
、-=
等方式对变量自增(这也是由于Rust的所有权规则决定的)。
let mut i = 3;
loop {
if i < 0 { break; }
i -= 1;
}
loop循环等价于while true {}
,是一个无限循环的循环体。
let mut rng = rand::thread_rng();
let random_number = rng.gen_range(-100, 101);
if random_number > 0 {
println!("random number is less than 0");
} else if random_number == 0 {
println!("random number is equal to 0");
} else {
println!("random number is greater than 0");
}
let mut i = 0;
let mut j = 0;
'outer : loop {
i += 1;
if i > 5 {
'inner : loop {
j += 1;
if j > 5 {
break 'inner
}
i = 0;
continue 'outer;
}
} else {
continue 'outer;
}
break 'outer;
}
println!("i = {i}, j = {j}");
我们可以在循环体前打上标记,如'outer
和'inner
。在break
和continue
时,可以追加标记,代表从何处继续。这个特性略类似于C/C++中的goto
语句,但是必须在循环体中使用。
enum Status {
Ok,
Fail,
}
fn main() {
let ret = Status::Ok;
match a {
Status::Ok => println!("is ok"),
Status::Fail => println!("is fail")
}
}
match
可以对枚举类型进行穷尽匹配,如果变量符合枚举值,则执行并返回=>
后的一段表达式。match
必须穷尽枚举类的所有枚举值,否则会报编译错误。对于枚举类型,match
的各项表达式返回值必须统一;
Rust中,我们可以通过添加注解来为sturct
或者enum
类型添加可打印的熟悉,如:
#[derive(Debug)]
struct A {
x: i32,
y: i32,
}
#[derive(Debug)]
enum Status {
Ok,
Fail,
}
let a = A{x: 1, y: 2};
let b = Status::Ok;
println!("{a:?}, {b:?}");
这里我们不需要主动为struct A
和enum Status
实现fmt::Display
特征,就可以通过println!
宏打印对象的内容。这种自省式的类似反射的功能是C++程序员梦寐以求的,但是C++目前仍然没有实现和支持静态反射,十分遗憾。
Rust中的字符串String
比较特殊,它是UTF-8
编码的字符串,是真的用来存储文本的一个对象。C++中的std::string
或者const char*
都只是保存或指向了一段以'\0'
结尾的内存空间(我倾向于称之为字节块)。
虽然Rust中的字符串String
底层也是一段字节块,但是它并不支持下标操作,因为如果随意选取一个下标,可能正好落在某个UTF-8
字符的中间,如果强行这么做,Rust程序会直接崩溃。但是String
提供了.chars()
方法,可以将底层字节块的内容按照UTF-8
的方式解析出来,并支持.iter()
方式迭代遍历。
C++目前主流的项目组织方式是CMake,但是大部分内容还是需要手动编写CMakeLists.txt文件才能生成所需的二进制文件。
Rust使用cargo作为项目组织工具,只需要一个配置文件Cargo.toml
即可全自动解决编译、依赖、版本管理(git)等功能,用过之后直呼太爽。
Rust要求可执行文件需要一个main.rs
文件,库文件要求一个lib.rs
文件。cargo会自动扫描目录下相关名称的文件,自动编译。
cargo new xxx
创建一个xxx目录,包含了基本可执行文件模板的项目结构目录,包含一个main.rs
和一个Cargo.toml
配置文件,;如果是cargo new xxx --lib
,则是按照库模板生成项目结构目录,包含一个lib.rs
和一个Cargo.toml
配置文件;cargo build
编译cargo run --
执行项目的可执行文件,--
两个横线后可以接命令行参数,传递给可执行文件;cargo doc --open
生成并打开项目文档;是的,你没有看错,只要在代码中按照规范写注释,就可以自动生成html文档。不像在C++ 中,还得手动搞doxygen,sphinx等等一大堆东西。cargo test [testname]
执行自动化(单元)测试,不需要额外搞gtest
或者boost.Test
,内置测试框架,直接一键测试。在Rust中,一个完整的项目称作crate
,官方的包管理库也叫crate.io
;在项目中,从逻辑上组织成一个统一功能的集合叫做package
,例如通过main.rs
生成的可执行文件属于一个package
,通过lib.rs
生成的库也是一个package
;每个rs
文件可以作为Rust项目的最小单位module
,在Rust源文件中,可以通过pub mod mymod
导出模块,通过use mymode
导入模块。
相比于C/C++的include模式,Rust的模块引入路径和项目组织有很大关系:
crate::
,通常这个根路径在项目内部可以省略;myproject::
,例如:lib.rs
导出的符号myfunc
,都可以通过路径use myproject::myfunc
或者use crate::myproject::myfunc
导入;lib.rs
里面导出的,通常放在和lib.rs
同一级的目录中,例如在lib.rs
中通过pub mod mymod
导出的子模块mymod
,它的源文件放在lib.rs
同一级目录的mymod.rs
中;mymod.rs
中导出的pub mod mysub
,它的源文件应当放在mymod/mysub.rs
中,如下所示:.
├── Cargo.toml
└── src
├── lib.rs // 使用了子模块 mymod
├── mymod
│ └── mysub.rs // mysub 的实现必须放在上级模块同名的目录下
└── mymod.rs // 子模块使用了子模块mysub
Rust对于引用有如下“铁律”,任何情况不得违反(智能指针除外,后面会讨论):
&mut
)或任意数量的不可变借用(&
)。和C/C++默认拷贝的语义不同的是,Rust中除了基本类型(类似于C/C++的POD),以及实现了Copy
特征的类型之外,一切赋值、或者函数参数形实结合的时候,都是移动语义,都会产生所有权的转移。
我们先看一段代码来感受一下:
fn main() {
let mut str = String::from("hello");
let ptr = &mut str;
let ptr2 = ptr;
*ptr2 = String::from("world");
*ptr = String::from("!"); // 编译错误!!!
}
在上面的代码中,我们首先定义了一个可变的字符串“hello”,然后通过引用ptr
获取了字符串str
的可变借用(指针);
然后,我们通过赋值定义了另一个引用ptr2
,注意,此时已经发生了所有权的转移:ptr
的内容(指针)转移给了ptr2
,ptr2
拥有了原本ptr
所“拥有”的指针。用C++的(近似)等价代码表示如下:
int main() {
std::unique_ptr<std::string> str(new std::string("hello");
std::unique_ptr<std::string> ptr(str.release());
std::unique_ptr<std::string> ptr2 = std::move(ptr);
*ptr2 = "world";
*ptr = "!";
// 最后,在离开作用域时,str会再次获得std::string的所有权,最后由str释放资源,即存在一段代码
// str.reset(ptr2.release()); 但是C++不存在这种等价行为,std::unique_ptr的所有权转移是唯一的
}
注意,此处只是尽可能模拟,并非完全还原真实情况。需要注意的是:
str
的时候,它拥有了这个字符串“hello”的所有权,等价于C++中通过std::unique_ptr
获取的指针所有权,str
拥有所拥有的对象的读、写以及所有权;ptr
引用指向&mut str
的时候,ptr
暂时获取了str
的读、写权限;注意,此时如果使用str
去修改字符串内容,是不允许的,根据“所有权铁律”,同一时间只能有一个可变借用,str
相当于对指向对象的可变借用,这个借用被转移给ptr
了;ptr2
,使其“等于”ptr
的时候,ptr
丧失了对str
的读写权限,转移到了ptr2
上面,此时通过ptr2
解引用修改或者读取str
的字符串是合法的;ptr
解引用时,由于ptr
丧失了str
的所有权,因此如果此时允许解引用,则会产生未定义行为;对应于C++中,当我们对已经使用过std::move
移除所有权的智能指针(将亡值)解引用时,也是未定义行为,但是C++编译期不会拒绝这样的代码,而Rust严格的标准会拒绝这样的代码。我们再回顾一下,Rust中的“引用”、“指针”、“借用”的概念,细细体会一下。在我的理解中,Rust中的变量在某种程度上都属于是“引用”,而“引用”是包含了一个“指针”的对象。当我们需要临时借用这个“引用”的“指针”所指向的对象时,我们就要发生“可变借用”或者“不可变借用”,此时,所有权仍然归第一个“引用”对象所有,但是我们可以获得这个对象的读或者写的权限,与此同时,第一个“引用”对于其所指向对象的读、写、所有权等权限可能会被暂时屏蔽。
(// TODO 待补充……)
Option
和Result
在Rust开发的错误处理规范中,我们会了解到Option
和Result
属于2类常见的枚举类,用于帮助确认函数的执行情况。
(// TODO 待补充……)
函数模板
类似C++的函数模板,但是需要注意的是,必须要为模板参数提供“特征”,否则只能原样返回入参。一个经典的练习题:
fn mystery<T>(x: T) -> T {
// ????
}
fn main() {
let y = mystery(3);
}
请问该函数的返回值
y
是多少?答案是
如果泛型类型的“模板参数”没有为其定义“特征”,那么这个泛型类型或者函数是没有任何意义的。
特征就是类似于Java中接口以及C++中抽象类的概念,它是一种特殊的类型,不能实体化,只提供一组方法的原型(也可以提供默认实现);
只要声明了对某个类型实现某个“特征”,那么这个类型就自动“继承”了这个特称下的一系列方法;在C++中,就相当于一个继承了抽象类的派生类实现。
特征实现的规则如下:
等价于C++中的lambda函数。基本语法形式:
fn main() {
let lambda = | | println!("hello world");
// 本质上完整的声明是
// | | -> () { println!("hello world") };
lambda();
}
和Rust函数声明对比:
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 ;
注意,add_one_v3
和add_one_v4
要求必须在程序中调用,才能推导出参数类型,才能编译通过。否则会产生编译错误。另外,Rust的闭包不支持泛型,也就是说,调用的参数只能推导出一种类型,一旦推导出类型之后,闭包不可以作用于其他类型的对象上。
对比C++的形式[&,=](auto arg) -> T { expr; }
可以看出,Rust的闭包缺少了捕获列表,实际上Rust会默认捕获其声明时所在作用域的所有对象,但是捕获的方式会根据闭包内表达式由编译器自动推导出三种情况:不可变借用,可变借用以及所有权转移。(后者对应C++14引入的移动捕获)
fn main() {
let list = vec![1, 2, 3];
println!("Before define closure, {:?}", list);
let borrow_immut = || prinltln!("Inside closure, {:?}", list);
println!("Before calling closure, {:?}", list);
borrow_immut();
println!("After calling closure, {:?}", list);
}
编译器根据闭包内的表达式println!("Inside closure, {:?}", list);
识别出list
只需要满足不可变借用即可。
// ---- snip ----
let mut list = vec![1, 2, 3];
println!("Before calling closure: {list:?}");
let mut borrow_mut = || list.push(7);
borrow_mut();
println!("After calling closure: {list:?}");
编译器根据闭包内的表达式list.push(7);
识别出list
应当满足可变借用才能正常工作。注意,此处的闭包类型也必须加上mut
,因为其中的list
是可变借用。
// ---- snip ----
let list = vec![1, 2, 3];
println!("Before defining closure: {:?}", list);
std::thread::spawn(move || println!("From thread: {:?}", list))
.join()
.unwrap();
注意,此处在std::thread::spawn
中,有一个move
关键字,显式声明了后面的闭包要求转移list
变量的所有权。
let f = |_|();
等价于std::mem::drop
,夺取入参的所有权并立刻丢弃入参。
Rust中的迭代器概念和C++中的差别很大。C++中的迭代器可以理解为一种特殊的指针,而Rust中的迭代器更加类似于Python中的迭代器,是一种可以遍历的、指向某些元素的一个集合。
基本用法示例:
// ---- snip ----
let v1 = vec![1, 2, 3];
let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();
println!("{:?}", v2);
let v3: Vec<_> = v2.into_iter().filter(|x| x > &2 ).collect();
println!("{:?}", v3);
let v3 =
代码中的map
和filter
都是函数式编程范式中的常用方法,需要注意的是,在Rust中,迭代器都是惰性的,因此产生之后必须消耗掉,否则会引发编译错误。通常使用collect
方法来消耗这些迭代器,保存到Vec
中。
注意,map
和filter
都是通过消耗前一个迭代器并生产新的迭代器,因此最后都要使用collect
来消耗这些迭代器。
迭代器在Rust中是一个实现了Iterator
特征的类型,其提供的next
方法返回了一个Option
类型。在遍历迭代器时,本质上我们做得是:
// ---- snip ----
let v = vec![1, 2, 3, 4];
let iter = v.iter();
while let Some(x) = iter.next() {
// do something with x
}
当遍历结束时,我们的Option
中只包含了一个None
,因此退出了while
循环。但是,在Rust中,遍历迭代器的方式建议是通过for循环:
// ---- snip ----
for x in v.iter() {
// do something with x
}
Rust中的迭代器是 一种零开销抽象(zero-overhead
),使用迭代器实现的代码性能不会比手动实现循环的代码慢。
在Rust中,通常我们说的指针实际上都是引用。或者换句话说,Rust中的引用取代了C++中指针的地位。需要注意的是,Rust中很多宏都会默认解引用,并且.
运算符也会解引用,因此通常情况下不需要主动解引用。手动解引用的方式和C/C++类似,都是*
号,例如:
let x = 1;
let y = &x;
*y += 1; // 手动解引用
println!("{}", y); // 默认解引用
在Rust中,智能指针有很多种,三方库也会提供自己的智能指针,我们只讨论标准库提供的最常见的3种智能指针:
Box
;(类似于std::unique_ptr
)Rc
;(类似于std::shared_ptr
)RefCell
访问的Ref
和RefMut
(Rust特有,用于运行时借用检查);Box
当我们需要从堆上申请内存时,最常见的方式就是使用Box
方法,他的地位就如同C语言的malloc
和C++的new
运算符一样,但是它本质上更类似于C++中的std::make_unique
。
例如,当我们需要为自定义类实现构造函数时,Rust中的惯用方法是这样的:
sturct Point {
x: i32,
y: i32,
}
impl Point {
fn new(x: i32, y: i32) -> Box<Self> {
Box::new( Point {x, y} )
}
}
fn main() {
let p = Point::new(1, 2);
drop(p); // 不可使用 p.drop();
}
这段代码有几个细节需要注意:
Point{x: x, y: y}
但是这里省略了冒号,因为这是Rust的构造省略特性,因为构造函数中的参数和成员的变量名重复了,就可以直接替代;new
函数第一个参数不是self
,它从功能上看类似于C++中的静态成员函数,因此也就不支持在对象上调用这个函数;如果要在对象上调用成员函数,那么其函数的第一个参数必须是self
或者&self
或&mut self
。main
函数结尾会对对象一一进行drop
操作,如果提前调用对象自己的drop
方法,则会造成double free
;如果确实要提前释放对象,需要使用std::mem::drop
方法。Box
类型p
本身是在栈上的一个对象,其中包含了一个指针(引用),指向了分配在堆上的Point
类型实例。其中堆上分配了Point
实例大小的内存用于存放这个Point
实例。
那么如何才能得知一个实例的大小呢?Rust中也提供了类似C++的sizeof
运算符的功能:
// ---- snip ----
println!("sizeof p is {}", std::mem::size_of_val(&p)); // 8 = 1个指针的长度
println!("sizeof Point is {}", std::mem::size_of::<Point>()); // 8 = 2个i32的长度
注意,在Rust中,有一个注解#[repr(C)]
用来表示以C语言ABI(二进制接口)的方式分配类型的内存布局,也是C/C++中的默认内存布局,通常是以4字节对齐的。如果需要实现C++中类似#pragma pack(push, 1)
这样的紧凑布局,则需要使用注解#[repr(packed)]
,一般在网络通信中常用。
Rust中定义析构函数的方式:
为类型实现Drop
特征,实现drop
方法
sturct A {
x: i32,
}
impl Drop for A {
fn drop(&nmut self) {
println!("Deconstruct A with data `{}`!", self.x);
}
}
fn main() {
let a = A{x: 1};
drop(a);
println!("bye");
}
Rc
在Rust中,Rc
是类似于std::shared_ptr
的东西,但是Rc
指涉的对象是不可修改的,因为Rc
没有实现DerefMut
特征,如果要修改,编译器会报错如下:
trait
DerefMut
is required to modify through a dereference, but it is not implemented forRc
注意,Rc
不是线程安全的,只能用于单线程。使用示例:
enum MyList {
Cons(i32, Rc<MyList>),
Nil,
}
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
注意上面的MyList
,其中的Cons
成员是一个包含了i32
和Rc
的元组。如果直接写为Cons(i32, MyList)
则会造成编译错误,因为产生了递归定义。这就类似于C++中定义一个二叉树节点,需要写成这个样子:
struct Node {
Node* left;
Node* right;
int data;
};
因为在定义Node
的时候,其大小还未确定,因此不能在其中声明Node
类型的成员,只能声明为指针;在这种情况下,Rust也是同理,Rc
也可以替换成Box
实现MyList
的定义。
需要说明的是,在Rust中,对于实现了Clone
特征的对象,通常是使用a.clone()
放法进行拷贝。由于clone()
方法在约定俗成上是深拷贝,而Rc
虽然是一个对象,但是作为引用计数智能指针,并不真正拥有其所指涉对象的所有权,所以不需要进行深拷贝;因此,Rust对于Rc
采用了另一种约定俗成的拷贝方式,即Rc::clone
。这个方式拷贝出来的Rc
对象,仅仅克隆了其内部的指针,然后增加了所指涉对象的引用计数。
RefCell
从基本作用上看,RefCell
和Box
是一样的,都是只能用于指涉一个自己拥有所有权的对象。但是,RefCell
却有着运行时控制引用方式的特性。一般来说,根据Rust的所有权“铁律”,编译器在编译期会通过借用检查来确保“铁律”通过。
Rust的编译器会执行借用检查,静态分析等机制对代码进行检查和优化。它的标准是非常严格的,因此宁愿错杀也不会放过任何可能导致出错的代码。当你认为你的代码是正确的,但是编译器无法理解时,就需要一些手段让编译器允许你的代码可以通过——RefCell
就是为了这个功能而生的。
注意,RefCell
和Rc
一样,不保证线程安全,只能用于单线程。
总结一下各个智能指针的特性:
由于RefCell
的借用检查是发生在运行时的,因此即便RefCell
对象本身是不可变的,但是其所指涉的对象在运行期也是可以修改的。
这种特殊的现象有点类似C++中,一个const对象,调用const成员函数,但是修改了标记为mutable
的成员的情况。这种情况在C++中不算常见,因为大部分引用都是可变的,而且可以同时拥有多种引用。但是在Rust中,由于严格的借用检查,这种场景就比较常见了。例如
use std::cell::RefCell;
struct Api {
count: RefCell<usize>
}
impl Api {
fn send(&self) {
*self.count.borrow_mut() += 1;
// 做一些其他事情
}
}
fn main() {
let a = Api{count: RefCell::new(0usize)};
a.send();
a.send();
a.send();
a.send();
println!("Api called {} times", a.count.borrow());
// 打印 Api called 4 times
}
注意,some_method
第一个参数是&self
是不可变的引用,但是我又需要统计对Api调用的次数,因此将count
成员通过RefCell
封装,我们可以通过不可变引用修改对象内部的值。在C++中,我们可以写出以下等价代码片段:
struct Api {
mutable size_t count{0};
void send() const {
count++;
// do something else
}
};
int main() {
Api inst;
const Api& a = inst;
a.send();
a.send();
a.send();
a.send();
return 0;
}
由于Rc
内部的对象必须是不可变引用,因此通常在Rust中可以使用Rc
封装的方式来创建可以修改的共享指针。
思考下列代码,可以帮助我们理解循环引用问题是如何产生的:
use crate::List::{Cons, Nil};
use std::{cell::RefCell, rc::Rc};
#[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,
}
}
}
fn main() {
let a = Rc::new(Cons(5, RefCell::new(Rc::new(Nil))));
// 注意此处 a 是 List::Cons(5, RefCell> -> 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))));
// 此处 b 是 List::Cons(10, RefCell> -> a )
// 从链表角度来看 就是 b -> a -> Nil
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);
}
// 由于 a.tail() 返回了 Some 就是 其中的 RefCell> -> Nil 这个指针
// if 中 a的这个指针修改了指向 -> b
// 此时从链表角度看 就是 b -> a -> b -> a.....
println!("b rc count after changing a = {}", Rc::strong_count(&b));
println!("a rc count after changing a = {}", Rc::strong_count(&a));
// 下面这行注释取消后 程序将会爆栈
// println!("a next item = {:?}", a.tail());
}
在main
函数返回后,a和b中RefCell
的引用计数都为1,导致了内存泄漏。
这种情况和C++中std::shared_ptr
互相指涉导致的循环引用问题如出一辙,通常是程序逻辑有问题,Rust编译器并不会检查出这种问题,要求程序员能够通过单元测试保证逻辑正确(C++中也是如此)。
Rust号称可以安全地处理并发编程,那么它是如何做到的呢?
(待续……)