目录
设计哲学
安全:内存安全与线程安全
内存安全:垃圾回收,自动引用计数
线程安全: 单线程, CSP模型,Actor模型
零成本抽象:不会为没使用的功能付出代价
实用性:能够用在各种场合; 类型推断,范型支持,模式匹配 ... other goodies
1 现有 c c++ 程序中内存安全问题分类
1、Dangling Pointer
2、Use After Free
3、Double Free
typedef struct Dummy {int a; int b; } Dummy;
void foo(void) {
Dummy *ptr = (Dummy *) malloc(sizeof (struct Dummy));
Dummy *alias = ptr;
free(ptr);
int a = alias.a; // when free ptr, alias is become a dangling pointer; and alias is used after free!!
free(alias); // Double free
}
2 Rust 解决内存安全的解决方法:所有权 + 借用
所有权
1、堆内存中的数据视为资源
2、堆上的资源只有一个所有者
3、当资源被其他代码借用时,资源所有者不可以释放资源或修改资源
4、编译时检查,无运行时开销
fn main () {
let x = String::from("hello"); //字符串 hello 的所有者为 x
let y = x; //字符串 hello 的所有者变成为 y, x 失去 所有权
// error[E0382]: use of moved value: 'x'
drop(x);
}
借用:rust其它代码,可以从资源的所有者处借用到资源,只有使用权,没有所有权
copy语义:
fn main() {
let x = 0u8;
let y = x; //是不会编译报错的,因为立即数,整数具有copy语言 let y = x 时复制了另一份数据给y
println!("{}", y);
}
借用
借用本质上是一个指针
借用分为可变借用,只读(共享)借用,可变借用可以用来修改指向对象的数据
资源分为可变资源,不可变资源,不充许对不可变资源进行可变借用
允许同时存在多个只读供借用指向同一对象
可变借用:拥有被借用对象的独占权,可变借用存在的情况下,不允许其它只读、可变借用存在
借用的生命周期小于被借用资源的生命周期
struct Dummy {a: i32, b: i32}
fn foo() {
let mut res = Box::new(Dummy{
a:0,
b:0
});
take(&res);
res.a = 2048; // take函数返回不会释放 res所指向的资源,因为arg是只读借用,且res是资源的所用者
}
// res资源被 arg 只读借用
fn take(arg: &Box<Dummy>) {
arg.a = 2048; // 编译错误: Cannot mutate via an immutable refrerence
}
语法学习:
只读借用: arg: &DataType
只读借用(&)
允许存在多个对同一资源的只读借用,但是只读借用存在期间不允许对资源进行任何修改
&运算符用来获取资源的只读借用
struct Dummy {a:i32, b:i32}
fn foo() {
let mut res = Box::new(Dummy{a:0, b:0});
{
let alias1 = &res;
let alias2 = &res;
let alias3 = alias2;
res.a = 2048; //编译报错:在大括号区间,存在对资源res的只读借用,不允许对资源进行修改
}
res.a = 2048;
}
可变借用(&mut)
struct Dummy {a:i32, b:i32}
fn foo() {
let mut res = Box::new(Dummy{a:0, b:0});
take(&mut res);
res.a = 4096;
let borrower = &mut res; // 可变借用
let alias = &mut res; // 编译报错:可变借用(borrower)对资源具有独占性, (在borrower生命周期内)不充许其它借用存在
}
fn take (arg: &mut Box<Dummy>) {
arg.a = 2048;
}
可变性
类似函数式语言,Rust中所有的资源都默认不可变
需要使用 mut 关键字声明某个资源为可变的
struct Dummy {a:i32, b:i32}
fn foo() {
let res = Box::new(Dummy{a:0, b:0});
res.a = 2048; // 错误:资源是不可变的
let borrower = &mut res; // 错误:不充许对不可变资源进行可变借用
}
生命周期
编译器使用生命周期对代码逻辑进行限制:借用的存活时间不应该比被借用对象长,解决悬垂指针问题
生命周期可以理解为变量作用域的长度,变量a比变量b 活得长 outlive 即指a的作用域能够涵盖b的作用域
Rust编译器会尽量窄化变量的生命周期
let a = 0;
let y = &x;
let z = &y;
自动类型推导,自动声明周期推导,解开语法糖
//Note: `'a: {` and `&'b x` is not valid syntax!
'a: {
let x : i32 = 0;
'b: {
//lifetime used is 'b because that's good enough.
let y : &'b i32 = &'b x;
'c: {
//ditto on 'c
let z: &'c &'b i32 = &'c y;
}
}
}
拒绝悬垂指针
拒绝迭代器失效
c++无法阻止程序迈向段错误
std::vector<int> v{0,1,2};
int& x = &v[0];
v.push_back(3); // vector在push数据,原来的存储空间不足时会重新分配一个更大的空间,将旧数据复制到新的空间,这时 x 指向的空间就可能是非法的出现段错误
std::cout << x << std::endl;
Rust试图拯救迷途的程序员
let mut data = vec![0,1,2];
let x = &data[0]; //desuger let x : i32 = Index::index::(&data, 0);
data.push(3); //desuger Vec::push(&mut data, 4) 编译报错,可变借用具有独占性,上面的那一行desuger之后用到了data的只读借用,不满足独占性,违反借用规则
println!("{}", x);
'a: { // 生命周期 'a
let mut data: Vec<i32> = vec![1,2,3];
'b: { //生命周期'b
// 'b is as big as we need this borrow to be (just need to get to `println!`)
let x: &'b i32 = Index::index::<'b>(&'b data, 0);
'c: {
//Temporary scope because we don't need the &mut to last any longer
Vec::push(&'c mut data, 4);
}
println!("{}", x);
}
}
借用 + 生命周期 = 痛苦之源
并发安全问题
数据竞争导致的
泛型与Trait
Rust使用Trait对类型的行为进行抽象
组合优于继承
Trait的作用
接口抽象
泛型参数约束
类型标签(Copy, Clone)
抽象类型
trait Greeting {
fn greet(&self);
}
struct Student;
struct Teacher;
impl Greeting for Student {
fn greet(&self) { println!("I'm Student");}
}
impl Greeting for Teacher {
fn greet(&self) {println!("I'm Teacher");}
}
fn greet<T: Greeting>(human: &T) {
human.greet();
}
数据竞争
Must be this tall to write multi-threaded code : Mozlia 公司的梗,多线程编程的难度
数据竞争:线程安全的一生之敌
Rust中多线程
多线程的相关内容位于std::thread模块中,可以直接使用spawn函数创建新的线程,spawn是对系统线程的封装
假如我们在多个Rust线程访问一处资源
use std::thead
fn main() {
let mut v: Vec<i32> = vec![];
thread::spawn(|| {
let l = v.len();
});
println!("{:?}", v);
}
//上面代码会编译报错
//Rust认为闭包具有静态生命周期与程序生命周期一样从生到死
//闭包会捕获外部变量来使用,该例子中闭包作为静态生命周期
//使用到 main 函数中 变量 v 的引用,团包认为 v 是 main 的局部变量
//v的生命周期没有静态生命周期长或一样
closure may outlive the current function, but it borrows 'v', which is owned by the current function
添加move关键字,转移v的所有权,至团包中
use std::thread
fn main() {
let mut v: Vec<i32> = vec![];
thread::spawn(move || {
let l = v.len();
});
println!("{:?}", v); //编译时会报错 v 的所有权已移至闭包中 不能再使用 v
}
//使用move关键字,将v的包有权移至闭包中
//
无畏并发
我们没有办法在多线程中直接使用普通的共享变量
大道至简:Rust创造性地使用类型系统来确保多线程程序中不存在数据竞争
两个特殊的Trait
std::marker::Send
如果类型T实现了Send类型,那说明这个类型的变量在不同线程中传递所有权是安全的
几乎所有的Rust类型都是Send,引用计数Rc<T>类型除外,只能用于单线程场景(为了零成本抽象)
若没有实现Send类型,若试图在线程间发送,编译时会报the trait Send is not implemented for ... 错误
std::marker::Sync
如果类型T实现了Sync,那说明在不同的线程中使用&T访问同一个变量是安全的
对于任意类型T,如果&T是Send的话T就是Sync的
基本类型是Sync的,安全由Sync的类型组成的类型也是Sync的,Rc<T>, Cell<T>, RefCell<T> 也不是Sync的,但是Mutex<T>是Sync的
不正确的实现 Send 和 Sync 会导致未定义的行为
//只要使用Rust提供的线程安全相关的设施,便能够安全地实现线程间状态共享
//最终实现版本
use std::thread;
use std::sync::{Mutex, Arc}; // Mutex 复制锁 Arc 原子引用计数器实现Send语义
fn main() {
let arc1 = Arc::new(Mutex::new(vec![]));
let arc2 = arc1.clone();
let t = thread::spawn(move || {
let mut v = arc2.lock().unwrap();
v.push(1);
});
t.join();
let v = arc1.lock().unwrap();
println!("{:?}", v);
}
双向链表困境
如何激怒一位Rust语言爱好者:让TA实现双向链表
//版本1
struct Node<T> {
data: T,
prev: Box<Node<T>>,
next: Box<Node<T>>,
}
//版本2
struct Node<T> {
data: T,
prev: Rc<Node<T>>,
next: Rc<Node<T>>,
}
//版本3
struct Node<T> {
data: T,
prev: Weak<Node<T>>,
next: Rc<Node<T>>,
}
//版本4
struct Node<T> {
data: T,
prev: Option<Weak<Node<T>>>,
next: Option<Rc<Node<T>>>,
}
//版本5
struct Node<T> {
data: T,
prev: Option<Weak<RefCell<Node<T>>>>,
next: Option<Rc<RefCell<Node<T>>>>,
}
Rust的解决方案: Unsafe
最小化unsafe代码块的范围
对unsafe代码需要进行更多思考及评审