Rust_所有权
文章目录
对极客时间陈天老师的Rust课程的笔记记录
三条准则
- 一个值只能被一个变量所拥有,这个变量成为所有者
- 一个值在同一时刻只能有一个所有者
- 当所有者离开作用域,这个值即被销毁
保证单一所有权
Move语义
Move语义就是第二条准则的体现
当一个变量赋值给第二个变量,那么这个数据的所有权就发生了转移,此时第一个变量就失效了,保证了在同一时刻,一个值只有一个所有者
传值传参数都会发生所有权的转移
如果不希望所有权被转移,可以使用Copy语义。如果一个数据结构实现了Copy trait,就会使用Copy语义,在赋值或者传参时,值会自动按位拷贝(浅拷贝)
如果不希望所有权被转移,又无法使用Copy语义,那可以借用数据
Copy语义
如果要move一个值,这个值的类型实现了Copy trait,就会自动使用Copy语义进行拷贝,否则使用Move语义进行移动。
符合Copy语义的类型,在赋值或传参时,值会自动按位拷贝(浅拷贝)
- 原生类型,包括原生类型的函数、不可变引用、裸指针实现了Copy
- 数组和元组,如果内部结构实现了Copy,那么他们也实现了Copy
- 可变引用没有实现Copy
- 非固定大小的数据结构,没有实现Copy
Borrow语义
Rust允许一个值的所有权在不发生转移的情况下,被其他上下文使用。好像住酒店,旅客只有房间的临时使用权,但没有所有权,通过&或者&mut来实现
不可变借用
默认情况下,Rust的借用都是只读的。住酒店在退房时要完好无损,而之后的可变引用就像是租房,可以对房子进行必要的装修。
Rust所有的参数传递都是传值,无论是Copy还是Move,所以在Rust中,必须显示地把某个数据的引用传给另一个函数。
看一个例子
fn main() {
let data = vec![1,2,3,4];
let data1 = &data;
println!("addr of value: {:p}({:p}), addr of data {:p},data1:{:p}"
,&data,data1,&*data,&data1);
println!("sum of data1: {}",sum(data1));
//堆上数据
println!("addr of items: [{:p},{:p},{:p},{:p}]",&data[0],&data[1],&data[2],&data[3]);
}
fn sum(data: &Vec<u32>) -> u32 {
println!("addr of value: {:p}, addr of ref: {:p}",data,&data);
data.iter().fold(0, |acc,x| acc + x,)
}
运行结果
addr of value: 0x7fffcdaff730(0x7fffcdaff730), addr of data 0x5562c8a07ad0,data1:0x7fffcdaff748
addr of value: 0x7fffcdaff730, addr of ref: 0x7fffcdaff548
sum of data1: 10
addr of items: [0x5562c8a07ad0,0x5562c8a07ad4,0x5562c8a07ad8,0x5562c8a07adc]
借用不能超过值的生命期,rust让我们只用关心调用栈的生命周期
可变借用
-
在一个作用域内,仅允许一个活跃的可变引用,活跃指的是真正被使用用来修改数据的可变引用,如果只是定义了,但没有使用或者只用来做只读引用,就不算活跃
-
在一个作用域内,活跃的可变引用和只读引用是互斥的,不能同时存在
- 一个作用域内,可以有多个只读引用
- 一个作用域内,可以有一个可变引用
Rc和Arc
问题提出:Rust使堆上某块内存在同一时刻只有一个引用有对它的所有权,那么
- 如果一个有向无环图DAG,某个节点可能有两个以上的节点指向它,如何表述
- 多个线程要访问同一块共享内存,如何使用
如上问题只在运行时才会遇到,编译期所有权的静态检查无法处理,如此Rust提供了运行时的动态检查
Rust使用引用计数的智能指针:Rc(reference counter)和Arc(Atomic reference counter)
Rc
对于某个数据结构T,我们可创建引用计数Rc,使其有多个所有者。Rc会把相应的数据结构创建在堆上,之后如果想对数据创建更多的所有者,可以通过clone来完成
对一个Rc结构进行clone,不会将其内部的数据复制,只会增加引用。当一个Rc结构离开作用域被drop时,也只会减少其引用,直到引用计数为0,才会真正清除对应内存。
use std::rc::Rc;
fn main() {
let a = Rc::new(1);
let b = a.clone();
let c = a.clone();
}
那么Rc如何逃出编译器的所有权冲突检查的呢?
我们调用clone返回了一个新的Rc,所以对于编译器,abc各自拥有一个Rc
Box::leak()机制
Rc之所以能够在堆上产生,不受栈内存生命周期的控制,就是因为Box::leak()机制,它可以创建不受栈内存控制的堆内存,生命周期大到和整个进程生命周期一致的对象。相当于程序员主动撕开了一个口子,允许内存泄漏,但他符合最小权限原则。
动态检查和静态检查
- 静态检查:通过编译器保证代码符合所有权规则
- 动态检查:通过Box::leak()在堆内存上开辟不受限的生命周期,然后在运行过程中,通过对引用计数的检查,保证这样的堆内存最终能够被释放。
用Rc实现DAG
use std::rc::Rc;
#[derive(Debug)]
struct Node {
id: usize,
downstream: Option<Rc<Node>>,
}
impl Node {
pub fn new(id: usize) -> Self {
Self {
id,
downstream: None,
}
}
pub fn set_downstream(&mut self, downstream: Rc<Node>) {
self.downstream = Some(downstream);
}
pub fn get_downstream(&self) -> Option<Rc<Node>> {
self.downstream.as_ref().map(|v| v.clone())
}
}
fn main() {
let mut node1 = Node::new(1);
let mut node2 = Node::new(2);
let mut node3 = Node::new(3);
let node4 = Node::new(4);
node3.set_downstream(Rc::new(node4));
node1.set_downstream(Rc::new(node3));
node2.set_downstream(node1.get_downstream().unwrap());
println!("node1:{:#?},node2:{:#?}", node1, node2);
}
RefCell
当我们创建完DAG后是无法修改的,因为Rc只是一个只读的引用计数器,无法拿到Rc内部数据结构的可变引用。如果想要修改这个数据结构,就要使用RefCell,绕过静态检查,在动态运行时对某个只读数据进行可变借用。
内部可变性
先说外部可变性,就是通过mut关键字进行声明。
想要对没有声明成mut的值或引用也想进行修改,就要使用RefCell。也就是说在编译器眼中,值是只读的,但是在运行时,这个值可以得到可变借用,从而修改内部的值。
use std::cell::RefCell;
fn main() {
let data = RefCell::new(1);
{
//获得可变数据
let mut v = data.borrow_mut();
*v +=1;
}
println!("data: {:?}", data);
}
之所以要用花括号,是因为在同一个作用域下,不能同时有活跃的可变借用和不可变借用,通过花括号缩小了可变借用的生命周期。
总结一下
实现可修改DAG
use std::cell::RefCell;
use std::rc::Rc;
#[derive(Debug)]
struct Node {
id: usize,
// 使用RefCell让节点可以被修改
downstream: Option<Rc<RefCell<Node>>>,
}
impl Node {
pub fn new(id: usize) -> Self {
Self {
id,
downstream: None,
}
}
pub fn set_downstream(&mut self, downstream: Rc<RefCell<Node>>) {
self.downstream = Some(downstream);
}
pub fn get_downstream(&self) -> Option<Rc<RefCell<Node>>> {
self.downstream.as_ref().map(|v| v.clone())
}
}
fn main() {
let mut node1 = Node::new(1);
let mut node2 = Node::new(2);
let mut node3 = Node::new(3);
let node4 = Node::new(4);
node3.set_downstream(Rc::new(RefCell::new(node4)));
node1.set_downstream(Rc::new(RefCell::new(node3)));
node2.set_downstream(node1.get_downstream().unwrap());
println!("node1: {:#?}, node2: {:#?}", node1, node2);
let node5 = Node::new(5);
let node3 = node1.get_downstream().unwrap();
node3.borrow_mut().downstream = Some(Rc::new(RefCell::new(node5)));
println!("node1: {:#?} ,node2: {:#?}", node1, node2);
}
Arc和Mutex/RwLock
多个线程访问同一块内存不能使用Rc解决,因为Rc为了性能,用的不是线程安全的引用计数器。而Arc实现了线程安全的引用计数器。
RefCell也不是线程安全的。要在多线程中使用内部可变性,就要使用Mutex和RwLock,互斥量和读写锁。