Rust内部可变性之RefCell

背景

在Rust中,每个对象(变量)的可见性与可变性均受到所有权的限制,一个对象只能有一个所有者。这个限制对于内存管理来说,无疑是一个非常友善的设计,因为只需要维护好所有者的生命周期,就可以对使用该对象的安全性进行清晰的管理。比如如果对一个对象进行赋值,则伴随者所有权的转让,原有对象失去控制权,可以放心的进行清理。

可见性与可变性主要体现在对引用的处理上增加了理解与使用的负担,在C语言中,指针的灵活性赋予了开发者施展奇技淫巧的空间,但同时把麻烦推给了开发者。Rust用所有权,生命周期等概念限制了对指针的过度使用,但对于初学者而言,难免感觉到这些限制矫枉过正,严重制约了想象力的施展。

针对引用,Rust提供了两种类型:

  • &:共享引用
  • &mut:可变引用

在编译阶段,Rust的行为是,同一作用域内,对于某一个对象的引用,只允许存在两种情况:要么只有一个可变引用,要么同时存在多个共享引用,共享引用不允许修改内容,可变引用才有修改权限。

比如:

struct Person {
  name: String,
  age: usize,
}

fn main() {
  let person = Person { name: "Joe Biden".to_string(), age: 79 };  
  let person_ref: &Person = &person;  
  person_ref.age = 83;
}

其中,

person_ref.age = 34;

编译失败,因为person_ref属于共享引用,并没有修改权限。

再看一个简单的例子:

fn main() {
    let x = 1;
    let y = &x;
    y = 2;
}

其中,

y=2;

报错,因为y属于共享引用。

编译阶段,Rust borrow checker会对对象修改进行检查,一旦监测到共享引用修改引用指向的内容就会报错,这个检查符合rust的设计原则,但是对于开发者的要求却显得过于苛刻。

作为面向系统开发的语言,大多数rust开发者很有可能是从C/C++系列迁移而来,在此类语言中,获得一个变量的引用,然后对其进行修改是一件非常正常的事情。

另一方面,由于 Rust 的 mutable特性, 一个结构体中的字段,要么全都是 immutable,要么全部是mutable,不支持针对部分字段进行设置。比如,在一个struct中,可能只有个别的引用需要修改,而其他变量并不需要修改,为了一个变量而将整个struct变为&mut也是不合理的。

作为语言规范,尽管Rust在设计范式上开诚布公,也给了开发者明确的预期,但是与大多数人长期形成的习惯进行对抗并非什么明智之举。毕竟,弱小和无知不是生存的障碍,傲慢才是。

还好Rust的founder们并不是傲慢的人,为了解决这个现实问题,专门引入了内部可变性。

所谓内部可变性,简单理解,就是赋予共享引用修改的权限,由于这个“赋予”行为是明确指定的,并未违反rust的设计原则。

内部可变性引入了Cell与RefCell两个wrapper。

示例

样例1:

use std::cell::Cell;

#[derive(Debug)]
struct Person {
 name: String,
 age: Cell<usize>,
}

fn main() {
 let person = Person { name: "Joe Biden".to_string(), age: Cell::new(79) };  
 let person_ref: &Person = &person;  

 println!("Age is : {:?}", person_ref);
 person_ref.age.set(83);
 println!("Age is : {:?}", person_ref);
}

样例2:

use std::cell::Cell;
fn main() {
    let x = Cell::new(1);
    let y = &x;
    y.set(2);
    println!("{}", x);
}

也可以使用RefCell来实现:
样例1:

use std::cell::RefCell;

#[derive(Debug)]
struct Person {
  name: String,
  age: RefCell<usize>,
}
fn main() {
  let person = Person { name: "Joe Biden".to_string(), age: RefCell::new(79) };  
  let person_ref: &Person = &person;  
  println!("Age is : {:?}", person_ref);
  *person_ref.age.borrow_mut() = 83;
  println!("Age is : {:?}", person_ref);
}

样例2:

use std::cell::RefCell;
fn main() {
    let x = RefCell::new(1);
    let y = &x;
    *y.borrow_mut() =2;
    println!("{}", x);
}

内部可变性违背了rust关于共享引用的约定,但是通过引入Cell与RefCell,这种走后门的行为是严格备案的,并没有违背原则。

既然Cell与RefCell都能够实现内部可变性,那这两者之间有什么差异呢?

Cell与RefCell差异

首先来看定义:

struct Cell<T> {
    value: UnsafeCell<T>, 
}

struct RefCell<T: ?Sized> {
    borrow: Cell<usize>,
    value: UnsafeCell<T>,
}

RefCell相比Cell,内部维护了一个包装对象的引用计数,当通过RefCell.borrow获取一个共享引用时,内部引用计数加一,当获取的引用离开作用域时,内部引用计数减一,当RefCell.borrow_mut获取一个可变引用时,首先检测引用技数是否为 0,如果为 0,正常返回,如果不为 0,直接 panic,其实RefCell.borrow时也会做类似的检测,当已经获取了可变引用也是直接 panic, 当然为了避免 panic,我们可以用 RefCell.try_borrowRefCell.try_borrow_mut 来获取一个 Result 类型。

由于Cell并未引入引用计数,所以Cell<T>需要满足T:Copy

impl<T> Cell<T> where T: Copy {

  const fn new(value: T) -> Cell<T>;
  
  // Returns a copy of the contained value.
  fn get(&self) -> T;
  
  // Sets the contained value.
  fn set(&self, val: T);
}

对于Cell而言,通过get获取到的是原有对象的拷贝,set则使用新的对象替换原有老对象。RefCell<T>没有这个约束,它的操作都是通过返回可变指针完成。

由于实现机制上的差别,Cell只能包装Copy类型,而RefCell能够包装任意类型,所以在不确定一个对象是否实现了Copy时,应该选择RefCell。

由于上述差异,RefCell更加常用,通常的做法是配合Rc,组成Rc<RefCell<T>>

限制

由于Cell与RefCell均未实现Sync,所以这两种类型均只能用于单线程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351

推荐阅读更多精彩内容