所有权是rust独特而核心的内容,但它到底讲了啥,有什么用?让我们来看看
所有权的目的是为了内存安全而发明的,内存安全包括如下内容:
- 防止内存不安全
- 编译器语法检查
- rust 不允许使用未定义内存,在编译阶段就不会通过
- 缓冲区溢出
- 编译期和运行期都有边界检查
- 所有权
- 非法操作内存
- 空指针
- 悬垂指针
- 编译器语法检查
所有权机制
rust的所有权可以保证内存安全和性能。性能主要来自对数据的复制是浅拷贝(拷贝栈内存),即只在栈上拷贝或移动指向数据(堆内存)的指针;而不是深拷贝,即拷贝保存在堆上的大量具体数据。
rust中的“赋值”,默认是值的剪切以及所有权的剪切。所有权可以理解成数据或内存的增删改查权。比如
let a = Box::new(5);
let b = a;
类似于C++
的
unique_ptr<int> a(new int(5));
auto b = move(a)
+---+---+---+---+---+ +---+---+---+---+---+
栈 | | a | | b | | | | a | | b | |
+---+-+-+---+---+---+ +---+---+---+-+-+---+
| |
+---+ +---+
| |
+---+-v-+---+ +---+-v-+---+
堆 | | 5 | | | | 5 | |
+---+---+---+ +---+---+---+
一个值的所有权被转移给另外一个变量绑定的过程,就叫作所有权转移
所有者的权力
所有者是指对资源(包括内存、文件等)的管理者,它拥有:
- [删] 控制资源的释放,即资源的删除权
- 出借所有权
- [改] 出借资源的改写权
- [查] 出借资源的咨询权
- 转移所有权
Copy与Clone
变量
对于 let y = x;
- 如果
x
的类型实现了Copy
特性,那么let y = x;
就是按位将x
复制给y
- 如果
x
的类型没有实现Copy
特性,那么let y = x;
就是移动,把x的值和所有权剪切给了y,x变成未初始化的变量- 即使
x
的类型实现了Clone
特性,let y=x
也是实行的移动(剪切) - 但可以用
let y = x.clone()
来显式的采用复制
- 即使
结构体与枚举体
自定义的结构体都没有实现Copy
特性,我们可以手动实现
#[derive(Copy, Clone)]
struct A {
a: i32,
b: i32,
}
结构体中的每一个元素都必须已经实现了
Copy
,才能给自定义结构装上Copy
特性
元组与数组
对于元组类型来说,其本身实现了Copy,如果元素均为复制语义类型,则默认是按位复制的,否则会执行移动语义
let a = ("hello".to_string(), "world".to_string());
let b = a;
因为a
中的元素都是移动类型,所以b = a
执行的是移动语义
let a = (1, 2, 3);
let b = a;
因为a
中的元素都是复制类型,所以b = a
执行的是复制语义
数组和Option类型与元组类型都遵循这样的规则:如果元素都是复制语义类型,也就是都实现了Copy,那么它们就可以按位复制,否则就转移所有权
绑定、作用域与生命期
绑定
let a = "hello".to_string();
let b = a;
- 标识符
a
和String
类型字符串"hello"通过 let 绑定在了一起,let绑定了标识符a和存储字符串"hello"的那块内存,从而a对那块内存拥有了所有权。 let b = a
,相当于让b
绑定a
的那块内存,让a
废弃
不可变与可变
- rust绑定默认为不可变
- 也就是
let a = 5
,a
不可以改变绑定的内存
- 也就是
- rust的可变绑定用
mut
关键字let mut a = 5
,mut
是修饰a
的,即mut a
可以改变绑定的内存
生命期
生命期就是一个变量的作用域
- let 创建的词法作用域
let a = "hello"; let b = "world";
rust 创建的词法作用域如下:
'a { let a = "hello"; 'b { let b = "world"; } }
- 每个
let
声明都会创建一个默认的词法作用域,该作用域就是它的生命期 - 当绑定在词法作用域中传递的时候,就会产生所有权的转移
- let绑定会创建新的词法作用域,如果有其他变量作为右值进行赋值操作,那么该变量因为进入了
let
创建的词法作用域,所以要么转移所有权,要么按位复制
- 每个
- 花括号创建词法作用域
match
创建词法作用域let a = Some("hello".to_string()); match a { ... }
- 当
match a
时,a
进入了match
的词法作用域,因为会执行move
- 绑定a是
Option<String>
类型,String
类型具有移动语义,所以Option<String>
就是移动语义类型
- 当
- 循环语句
- for、loop以及while循环语句均可以创建新的作用域
let v = vec![1, 2, 3]; for i in v { ... }
绑定v为移动语义类型,进入for循环时已经转移了所有权
- for、loop以及while循环语句均可以创建新的作用域
if let
块和while let
块if let
块会创建一个新的作用域
- 函数
- 闭包捕获环境变量有三种方式
- 对复制语义类型,以不可变引用(
&T
)来捕获 - 对于移动语义类型,执行移动语义(
move
)转移所有权来捕获let s = "hello".to_string(); let join = |i: &str| s + i; asset_eq!("hello, world", join(" world"));
- 对于可变绑定,如果在闭包中包含对其进行修改的操作,则以可变引用(
&mut
)来捕获
- 对复制语义类型,以不可变引用(
引用
- 引用(Reference)是 Rust 提供的一种指针语义。
- 引用是基于指针的实现,它与指针的区别是
- 指针保存的是其指向内存的地址,
- 引用可以看作某块内存的别名(Alias)
- 使用它需要满足编译器的各种安全检查规则。
- 引用分为
- 可变引用
&mut
- 不可变引用
&
- 可变引用
- 引用会限制所有者
- 在不可变引用
&
期间,- 所有者不能修改资源
- 并且也不能再进行可变借用
- 在可变借用期间
- 所有者不能访问资源
- 并且也不能再出借所有权
- 在不可变引用
- 借用规则
- 借用的生命周期不能长于出借方(拥有所有权的对象)的生命周期
- 不能有多个可变借用(唯一可写)
- 不可变借用(引用)不能再次出借为可变借用(可写不可读)
同一时刻,只能拥有一个写锁,或者多个读锁
+-----------------+
| lock +--read +-->
+--- write------> memory lock +--read +-->
| lock +--read +-->
+-----------------+
+-----------------+
| +----- read ----->
+--- write--->lock memory +----- read ----->
| +----- read ----->
+-----------------+
fn compute(input: &u32, output: &mut u32) {
let cache_input = *input;
...
}
优化:因为 input 是一个不可变借用,永远都不会改变,所以编译器可以将它的值保存在寄存器中,进一步提升性能
Rust的借用检查带来了如下好处:
- 不可变借用保证了没有任何指针可以修改值的内存,便于将值存储在寄存器中。
- 可变借用保证了在写的时候没有任何指针可以读取值的内存,避免了脏读
- 不可变借用保证了内存不会在读取之后被写入新数据
- 保证了不可变借用和可变借用不相互依赖,从而可以对读写操作进行自由移动和重新排序
解引用操作会获得所有权
let s: &String = &"hello".to_string();
let a = *s;
let a = *s
会把String
类型转移给a
生命期
生命期用于rust的借用检查器推断借用的合法性,rust要确保左右的借用都是有效的,防止产生悬垂指针。
rust中变量的生命期长度用'a
表示,只有在出借方的生命期大于借用方的生命期长度,才不会造成悬垂指针。
显式生命期参数
生命期参数格式形如:'a
,位于&
之后
函数签名中的生命期参数
fn foo<'a>(s: &'a str, t: &'a str) -> &'a str;
函数或方法参数的生命期叫作“输入生命期”,返回值的生命期称为“输出生命期”,输出生命期 < 输入生命期。
fn the_longest<'a, 'b:'a>(s1: &'a str, s2: &'b str) -> &'a str;
's1 's2
^ ^
| |
+ +
let res = the_longest(&s1, &s2);
+-+-+ +---------+---------+
| |
v v
'r 't
the_longest
函数的第一个参数是&s1
,我们将其生命期命名为's1
,the_longest
函数签名中的泛型参数'a
在调用时被单态化为了's1
- 第二个参数
&s2
的生命周期我们将其命名为's2。同理,可以说,泛型参数'b
在此时被单态化为了's2
the_longest(s1_r,&s2)
返回引用的生命周期记为't
- let声明了绑定
res
,其生命周期记为'r
- 因为
res
绑定了the_longest
函数返回的借用,本质上是引用的按位复制,所以res成为了借用方,其生命周期长度为'r
,且等于't
- 因为
生命期的目的
生命周期参数的目的是帮助借用检查器验证合法的引用,消除悬垂指针
结构体定义中的生命周期参数
结构体在含有引用类型成员的时候也需要标注生命周期参数
struct Foo<'a> {
part: &'a str,
}
结构体实例的生命周期应短于或等于任意一个成员的生命周期:L(Foo) < L(Foo.part)
let f = Foo {part: frist};
用first
实例化结构体Foo
时,编译器就会根据结构体定义的生命期规则对其成员part的生命周期长度进行检查。要求L(f) < L(Foo.part)
,满足这个检查即可保证没有悬垂指针
方法定义中的生命周期参数
为带生命期参数的结构体实现方法,方法也要带上生命期参数
impl<'a> Foo<'a> {
...
}
静态生命周期参数
作静态生命周期。'static
生命期存活于整个程序运行期间。所有的字符串字面量都有'static
生命期,类型为 &'static str
。全局静态类型,它的数据和程序代码一起存储于可执行文件的数据段中,其地址在编译期是已知的,并且是只读的,无法更改
let a = "hello, world";
let b = a; // 此处赋值执行的是按位复制,而非移动语义
因为静态字符串按位复制的仅仅是存储于栈上的地址,因为数据段是只读的,并不会出现什么内存不安全的问题
省略生命周期参数
Rust针对某些场景确定了一些常见的模式,将其硬编码到Rust编译器中,以便编译器可以自动补齐函数签名中的生命周期参数,这样就可以省略生命周期参数。被硬编码进编译器的模式称为生命周期省略规则(Lifetime Elision Rule),一共包含三条规则:
- 每个输入位置上省略的生命周期都将成为一个不同的生命周期参数
- 如果只有一个输入生命周期的位置(不管是否忽略),则该生命周期都将分配给输出生命周期
- 如果存在多个输入生命周期的位置,但是其中包含着
&self
或&mut self
,则self
的生命周期都将分配给输出生命周期
生命周期限定
T:'a
,表示T类型中的任何引用都要“活得”和'a
一样长T:Trait+'a
,表示T类型必须实现Trait,并且T类型中任何引用都要“活得”和'a
一样长
trait对象的生命周期
一个trait对象中实现trait的类型带有生命周期参数
trait Too{};
struct Boo<'a> {
x: &'a i32,
}
impl<'a> Too for Boo<'a>{}
fn main() {
let num = 5;
let box_boo = Box::new(Boo{x :&num});
let trait_obj = box_boo as Box<Too>;
}
trait对象默认遵循的生命周期规则:
- trait对象的生命周期默认是
'static
- 如果实现trait的类型包含
&'a x
或&'a mut x
,则默认生命周期就是'a
- 如果实现trait的类型只有
T:'a
,则默认生命周期就是'a
- 实现trait的类型包含多个类似
T:'a
的从句,则生命周期需要明确指定
智能指针与所有权
智能指针拥有资源的所有权,普通引用只是对所有权的借用
Box<T>
智能指针也可以使用解引用操作符进行解引用,比如:
let a = Box::new("hello");
let b = Box::new("hello".to_string());
let a1 = *a; // 之后a还可以继续访问
let b1 = *b; // 之后b为未初始化状态
智能指针可以解引用是因为它们实现了Deref
方法,返回的是&T
类型。所以*a
相当于*(a.deref)
, *b
相当于 *(b.deref)
这种对Box<T>
使用操作符*
进行解引用而转移所有权的行为,被称为解引用移动,未来会有使用trait DerefMove
定义此行为。注意Rc<T>
和Arc<T>
不支持解引用移动。比如:
let a = Rc::new("hello");
let a1 = *a; // 这是不允许的
// 错误:不能把内容移出容器
Box<T>
和其他智能指针相同的地方在于内部都使用了box
关键字来进行堆分配,但它代表所有权唯一的智能指针
box
关键字会调用内部堆分配方法exchange_malloc
和堆释放方法box_free
进行堆内存管理
共享所有权Rc和Weak
Rc<T>
引用计数容器,
- 可以把所有权共享给多个变量
- 用于希望共享堆上分配的数据可以供程序的多个部分读取的场景
- 并且确保共享的资源析构函数都能被调用到,借用
&T
不会调用析构函数 x.clone()
得到强引用,即析构权被复制了Rc::downgrade(&x)
得到的是弱引用,它共享的智能指针没有析构权
let x = Rc::new(5);
let x1 = x.clone();
let x2 = x.clone();
// strong count: 3
println!("strong_count: {:?}", Rc::strong_count(&x));
let w = Rc::downgrade(&x);
// strong count: 1
println!("weak_count: {:?}", Rc::weak_count(&x));
let y = &*x;
// strong count: 3
println!("strong_count: {:?}", Rc::strong_count(&x));
// strong count: 1
println!("weak_count: {:?}", Rc::weak_count(&x));
内部可变性Cell和RefCell
- Rust中的可变或不可变主要是针对一个变量绑定而言的。
- 对于结构体,可变或不可变只能对其实例进行设置,不能设置单个成员的可变性
- 实际中,某个字段可变而其他字段不可变的情况也存在
- Rust提供了
Cell<T>
和RefCell<T>
来应对 - 它们本质上只是可以提供内部可变性(Interior Mutability)的容器
Cell
内部可变性容器是对Struct的一种封装,表面不可变,但内部可以通过某种方法来改变里面的值
let x = Cell::new(5);
assert_eq!(x.get(), 5);
x.set(10);
assert_eq!(x.get(), 10);
- 对于
Cell<T>
中的类型T
,只有实现了Copy
的类型T,才可以使用get方法获取包裹的值,因为get方法返回的是对内部值的复制 - 对于没有实现Copy的类型T,则提供了
get_mut
方法来返回可变借用 - 任何类型T都可以使用set方法修改其包裹的值
RefCell
RefCell<T>
有borrow/borrow_mut
方法,对应Cell<T>的
get/set`方法RefCell<T>
有运行时开销,因为它自己维护着一个运行时借用检查器Cell<T>
和RefCell<T>
之间的区别- 读写
Cell<T>
使用set/get
方法直接操作包裹的值RefCell<T>
通过borrow/borrow_mut
返回包装过的引用Ref<T>
和RefMut<T>
来操作包裹的值
Copy
和Move
Cell<T>
一般适合复制语义类型(实现了Copy)RefCell<T>
一般适合移动语义类型(未实现Copy)
- 运行时开销
Cell<T>
无运行时开销RefCell<T>
需要在运行时执行借用检查,所以有运行时开销
- 引发
panic
Cell<T>
永远不会在运行时引发panic
RefCell<T>
一旦发现违反借用规则的情况,则会引发线程panic而退出当前线程
- 读写
写时复制Cow
写时复制(Copy on Write)技术是一种程序中的优化策略,只有等到真正需要写入的时候才复制。
Cow<T>
是一个枚举体的智能指针,包括两个可选值:
Borrowed
,用于包裹引用Owned
,用于包裹所有者Option<T>
表示的是值的“有”和“无”,而Cow<T>
表示的是所有权的“借用”和“拥有”
Cow<T>
提供的功能是
- 以不可变的方式访问借用内容
Cow<T>
实现了Deref
,所以可以直接调用其包含数据的不可变方法
- 在需要可变借用或所有权的时候再克隆一份数据
- 借用的数据被克隆为的新的对象
let s1 = [1, 2, 3, 1, 2];
let mut i1 = Cow::from(&s1[..]);
abs_all(&mut i1);
因为所有值都大于0,没有变化,所以这里没有发生数据的复制
let s2 = [1, 2, 3, -1, 2];
let mut i2 = Cow::from(&s2[..]);
abs_all(&mut i2);
因为s2[3] = -1
, abs_all
会修改内容,所以这里发生了复制
let s3 = Cow::from(vec![1, 2, 3, -1, 2]);
abs_all(&mut s3);
因为数据本身拥有所有权,所以不会复制
使用Cow
时,需要掌握以下几个要点:
Cow<T>
实现了Deref
,所以可以直接调用T的不可变方法- 在需要修改 T 时,可以使用
to_mut
方法来获取可变借用- 该方法会产生克隆,但仅克隆一次,如果多次调用,则只会使用第一次的克隆对象
- 如果T本身拥有所有权,则此时调用
to_mut
不会发生克隆
- 在需要修改T时,也可以使用
into_owned
方法来获取一个拥有所有权的对象- 如果T是借用类型,这个过程会发生克隆,并创建新的所有权对象
- 如果T是所有权对象,则会将所有权转移到新的克隆对象
并发安全与所有权
Rust的两个trait——Send
和Sync
- 如果类型
T
实现了Send
,说明该类型的实例可以在线程间安全传递所有权 - 如果类型
T
实现了Sync
,表明该类型的实例在多线程并发中可以安全地跨线程共享
rust 线程安全的同步机制:
Arc<T>
是线程安全版本的Rc<T>
Mutex<T>
是锁,同一时间仅允许有一个线程进行操作RwLock<T>
相当于线程安全版本的RefCell<T>
,可以同时运行多个reader
或者一个writer
Atomic
系列类型相当于线程安全版本的Cell<T>
非词法作用域生命周期
非词法作用域生命周期(Non-Lexical Lifetime,NLL),基于MIR的借用检查
MIR是基于控制流图(Control Flow Graph,CFG)的抽象数据结构,基于MIR的借用检查称为非词法作用域的生命周期