当前位置: 首页 > 科技观察

为什么需要内部可变性

时间:2023-03-15 18:40:17 科技观察

本文参考了rustbookch15,加上自己的理解。有兴趣的可以先看看官方文档Rust有两种方式实现mutability继承可变性:比如声明一个struct的时候,指定letmut,那么后面就可以修改这个结构体任意字段的内部可变性:使用CellRefCell来包裹变量或字段,这样即使外部变量是只读的,也可以对其进行修改。看似遗传的可变性就足够了,那为什么还需要它呢?所谓的内部可变性呢?让我们分析两个例子:x+self.y);self.sum.unwrap()},Some(sum)=>sum,}}}fnmain(){leti=Cache{x:10,y:11,sum:None};println!("sumis{}",i.sum());}结构Cache有三个字段,x,y,sum,其中sum模拟lazyinit懒加载模式,上面的代码运行不了,原因很简单,当变量i被let初始化时,它是不可变的。17|leti=Cache{x:10,y:11,sum:None};|-帮助:考虑将其更改为可变的:`muti`18|println!("sumis{}",i.sum());|^cannotborrowasmutableYes有两种方法可以解决这个问题。letmuti是在声明let的时候指定的,但是对于具体的大项目,外层变量很可能是不可变的。这就是内部可变性派上用场的地方。修复usstd::cell::Cell;structCache{x:i32,y:i32,sum:Cell>,}implCache{fnsum(&self)->i32{matchself.sum.get(){None=>{self.sum.set(Some(self.x+self.y));self.sum.get().unwrap()},Some(sum)=>sum,}}}fnmain(){leti=缓存{x:10,y:11,sum:Cell::new(None)};println!("sumis{}",i.sum());}这是修复后的代码,求和类型为Cell。其实每一个都是有意义的。比如Rc代表共享所有权,但是因为Rc中的T要求是只读的,不能修改,所以需要用Cell封起来,这样所有权是共享的,但是还是可变的。Option是一个普通值,要么有值Some(T),要么有空值None,这个很好理解。如果不写rust代码,只想看源码了解流程,没必要深究这些wrapper,只关注包的真实类型即可。官网给出的例子是MockObjects,代码比较长,但是原理是一样的。structMockMessenger{sent_messages:RefCell>,}最后用RefCell包裹了结构字段。Cellusestd::cell::Cell;fnmain(){leta=Cell::new(1);letb=&a;a.set(1234);println!("bis{}",b.get());}这段代码很有代表性。如果变量a没有被Cell包裹,那么在存在b的只读借用的情况下,不允许修改a。它由Rust编译器在编译时保证:给定一个对象,在范围内(NLL)只允许N次不可变借用或一次可变借用。Cell通过get/set获取和修改值。此函数要求值必须实现Copy特征。如果我们将其替换为其他结构,编译时会报错。错误[E0599]:themethod`get`existsforreference`&Cel??l`,butitstraitboundswerenotsatisfied-->src/main.rs:11:27|3|structTest{|------------不满足`Test:Copy`...11|println!("bis{}",b.get().a);|^^^|=注意:不满足以下特征边界:`Test:Copy`可以从上面可以看出structTestCopy默认是没有实现的,所以get是不允许的。有没有办法获得底层结构?可以使用get_mut来返回底层数据的引用,但是这需要对整个变量进行letmut,所以不符合使用Cell的初衷,所以对于Move语义的情况,rust提供了RefCell。RefCell不同于Cell。我们使用RefCell通过borrow获取不可变借用,或者borrow_mut获取底层数据的可变借用。usestd::cell::{RefCell};fnmain(){letcell=RefCell::new(1);letmutcell_ref_1=cell.borrow_mut();//Mutabloborrowtheunderlyingdata*cell_ref_1+=1;println!("RefCellvalue:{:?}",cell_ref_1);letmutcell_ref_2=cell.borrow_mut();//Mutablylborrowthedataagain(cell_ref_1isstillinscopethough...)*cell_ref_2+=1;println!("RefCellvalue:{:?}",cell_ref_2);}代码来自badboi.dev,编译成功,但运行失败。#cargobuildFinisheddev[unoptimized+debuginfo]目标在0.03s##cargorunFinisheddev[unoptimized+debuginfo]目标在0.03sRunning`target/debug/hello_cargo`RefCellvalue:2thread'main'panickedat'alreadyborrowed:BorrowMutError',s/main.rs:10:31note:runwith`RUST_BACKTRACE=1`environmentvariabletodisplayabacktracecell_ref_1调用borrow_mut获取变量借用。当它还在范围内时,cell_ref_2也想获得一个变量借用。这个时候runtimecheck报错,直接panic。也就是说,RefCell会从编译时借用borrow规则到runtime运行时,有一定的运行时开销。#[derive(Debug)]enumList{Cons(Rc>,Rc),Nil,}usecrate::List::{Cons,Nil};usestd::cell::RefCell;usestd::rc::Rc;fnmain(){letvalue=Rc::new(RefCell::new(5));leta=Rc::new(缺点(Rc::clone(&value),Rc::new(无)));letb=Cons(Rc::new(RefCell::new(3)),Rc::clone(&a));letc=Cons(Rc::new(RefCell::new(4)),Rc::clone(&a));*value.borrow_mut()+=10;println!("aafter={:?}",a);println!("bafter={:?}",b);println!("cafter={:?}",c);}这是一个官方的例子,通过Rc和RefCell的结合使用,可以实现共享所有权,同时可以修改List节点的值。总结内部可变性提供了很大的灵活性,但考虑到运行时开销,它不能被滥用。性能问题不大。关键是少了编译时的静态检查,会掩盖很多错误。