Rust_所有权

Rust_所有权



对极客时间陈天老师的Rust课程的笔记记录

三条准则

  • 一个值只能被一个变量所拥有,这个变量成为所有者
  • 一个值在同一时刻只能有一个所有者
  • 当所有者离开作用域,这个值即被销毁

保证单一所有权

Move语义

Move语义就是第二条准则的体现

当一个变量赋值给第二个变量,那么这个数据的所有权就发生了转移,此时第一个变量就失效了,保证了在同一时刻,一个值只有一个所有者

传值传参数都会发生所有权的转移

如果不希望所有权被转移,可以使用Copy语义。如果一个数据结构实现了Copy trait,就会使用Copy语义,在赋值或者传参时,值会自动按位拷贝(浅拷贝)

如果不希望所有权被转移,又无法使用Copy语义,那可以借用数据

Copy语义

如果要move一个值,这个值的类型实现了Copy trait,就会自动使用Copy语义进行拷贝,否则使用Move语义进行移动。

符合Copy语义的类型,在赋值或传参时,值会自动按位拷贝(浅拷贝)

  • 原生类型,包括原生类型的函数、不可变引用、裸指针实现了Copy
  • 数组和元组,如果内部结构实现了Copy,那么他们也实现了Copy
  • 可变引用没有实现Copy
  • 非固定大小的数据结构,没有实现Copy

Copy in std::marker - Rust (rust-lang.org)

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]

image-20210930164632547

借用不能超过值的生命期,rust让我们只用关心调用栈的生命周期

img

可变借用

  • 在一个作用域内,仅允许一个活跃的可变引用,活跃指的是真正被使用用来修改数据的可变引用,如果只是定义了,但没有使用或者只用来做只读引用,就不算活跃

  • 在一个作用域内,活跃的可变引用和只读引用是互斥的,不能同时存在

    • 一个作用域内,可以有多个只读引用
    • 一个作用域内,可以有一个可变引用

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();
}

img

那么Rc如何逃出编译器的所有权冲突检查的呢?

我们调用clone返回了一个新的Rc,所以对于编译器,abc各自拥有一个Rc

Box::leak()机制

Rc之所以能够在堆上产生,不受栈内存生命周期的控制,就是因为Box::leak()机制,它可以创建不受栈内存控制的堆内存,生命周期大到和整个进程生命周期一致的对象。相当于程序员主动撕开了一个口子,允许内存泄漏,但他符合最小权限原则。

img

动态检查和静态检查
  • 静态检查:通过编译器保证代码符合所有权规则
  • 动态检查:通过Box::leak()在堆内存上开辟不受限的生命周期,然后在运行过程中,通过对引用计数的检查,保证这样的堆内存最终能够被释放。
用Rc实现DAG

img

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);
}

之所以要用花括号,是因为在同一个作用域下,不能同时有活跃的可变借用和不可变借用,通过花括号缩小了可变借用的生命周期。

总结一下

image-20211002103520391

实现可修改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,互斥量和读写锁。

总结

img

img

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值