大白话rust所有权

568 阅读5分钟

所有权并非rust独有,却是rust发扬光大并贯穿始终的特性,但它又是那么难理解,这个特性跟漫画《全职猎人》里,誓约与制约的概念类似,遵守誓约和制约换取更大的能力。

The Book里的原文如下

嗯,字母我都懂,翻成中文也难度不大,问题是它还是太难懂了。。

那,我换个方式讲一讲,业主与租客的关系。

  • 一间房同一时间只能有唯一一个业主(假设房产证写一个人名的情况下),这个人称为房屋所有人,但是所有跟使用未必是同一个人。
  • 业主可以把房子租出去,这时候房子的使用者变成了承租人(即rust里的借用),这时候分为两种情况,业主同意承租人更改房屋装修(即可变借用),这种情况一般是比较稳定的家庭,可以看做了长租,仅当租给一人或一个家庭(即可变借用仅能同时出借给1个使用者)这种情况,房东未经允许是不能再进这个房子的;另一个情况是业主租给一群小朋友轰趴,这时候这群小朋友并不能对房屋进行任何更改(即不可变借用可以多个使用者,但不允许修改原变量);无论业主出租房屋是那种情况,业主在出租期间都不能再对房屋进行任意更改(即出借后到出借结束,禁止修改原变量)
  • 房屋可以买卖,房子卖掉后,原业主肯定不能再持有原房屋(即赋值会造成所有权转移)
  • 业主瞎折腾各种贷款,资不抵债,房子被没收(即离开作用域自动释放内存)

rust的变量定义使用的是关键字let,而不是var或者类型+变量名,这是因为在rust看来,变量只是一个标识符,而变量定义被看作是值的绑定(binding),所有的值都需要有唯一合法的所有者,称为所有权机制。

到这里,大概规则都比较清晰了,但rust编译器为了你的使用,做了些貌似违反所有权规则的事:

fn main() {
 let mut person1 = "lilei";
 let person2 = person1;//发生了移动
 let person1 = "hanmeimei";//发生了修改
 println!("{}",person1);//输出hanmeimei
 println!("{}",person2);输出lilei
}

What?不是说好了赋值运算会产生所有权转移吗?print person1的时候编译器应该提示person1不合法才对啊?对,也不对。rust对于基础数据类型,如数字,char,&str等都是默认实现了copy,即直接按位复制,不会产生转移,所以这里还能继续访问person1,对于栈内存来说,直接按位复制代价是很低的(有兴趣的同学可以去了解一下汇编)。

rust 2018 edition后,新增了Non-lexical lifetimes非词法生命周期,使得编译器的操作看起来更加“违反”规则:

fn main() {
    let mut foo = 2;
    let bar = &mut foo;//发生可变借用,理论上不能再次访问foo
    println!("{}",bar);
    println!("{}",foo);
    // println!("{}",bar);//取消这行会报错
}

不是说可变借用只能存在一个吗?为什么还能继续打印foo,不是说好的房东不能再进门吗?其实非词法生命周期在这里判定了bar在print的时候已经释放了,所以foo是可以继续访问的,并没有违反规则,这个做法是为了减少使用者的在日常编码中的心智负担,提升编写效率。

理解一个事物,从它的因果出发,是最高效的。为什么rust编译器要如此严格的要求开发者遵守规则?一切都是为了内存安全。悬垂指针引发的未定义行为,释放后访问原位置的错误,数据竞争等,常见c系手动管理内存的问题,在rust这里被严格禁止,这就需要所有权的引入和规范。

但是,对于艺高人胆大的开发者,rust依然提供了两个方式绕过所有权机制,

  • 完全的unsafe code,这种方式跟直接写c用裸指针操作内存差不多。

  • 使用Cell和RefCell再套一层,其中Cell包裹的值,需要实现Copy,即实际上你操作的是一个按位复制的值,使用get,set来访问和更改它;RefCell更常见,使用的时候,用borrow(不可变借用)和borrow_mut(可变借用)来借用他,这时候编译器不再管你怎么借用包裹的值,运行时违法规则直接panic,线程崩溃,如果需要同时不可变借用和可变借用,再套一层Rc指针,使用如下:

    use std::cell::RefCell; use std::rc::Rc;

    fn main() { let vl = RefCell::new(vec![]); let rc = Rc::new(vl);

    rc.borrow_mut().push(2);//可变借用1
    rc.borrow_mut().push(3);//可变借用2
    
    println!("{:?}", rc.borrow());//不可变借用
    

    }

Rc包裹的值可以在单线程内共享引用,这样就可以绕过借用检测器。那么,为什么Cell和RefCell的设计难道不是和所有权规则相悖吗?并不是,因为所有权的初衷是为了内存安全,而引入这两个容器来绕过借用检查,是符合内存安全的,使用者并不是直接对值进行操作,而是被包裹了一层。