背景
在Rust中,每個(gè)對(duì)象(變量)的可見性與可變性均受到所有權(quán)的限制确垫,一個(gè)對(duì)象只能有一個(gè)所有者冗茸。這個(gè)限制對(duì)于內(nèi)存管理來說贞盯,無疑是一個(gè)非常友善的設(shè)計(jì),因?yàn)橹恍枰S護(hù)好所有者的生命周期草戈,就可以對(duì)使用該對(duì)象的安全性進(jìn)行清晰的管理。比如如果對(duì)一個(gè)對(duì)象進(jìn)行賦值,則伴隨者所有權(quán)的轉(zhuǎn)讓枯怖,原有對(duì)象失去控制權(quán),可以放心的進(jìn)行清理能曾。
可見性與可變性主要體現(xiàn)在對(duì)引用的處理上增加了理解與使用的負(fù)擔(dān)度硝,在C語(yǔ)言中,指針的靈活性賦予了開發(fā)者施展奇技淫巧的空間寿冕,但同時(shí)把麻煩推給了開發(fā)者蕊程。Rust用所有權(quán),生命周期等概念限制了對(duì)指針的過度使用驼唱,但對(duì)于初學(xué)者而言藻茂,難免感覺到這些限制矯枉過正,嚴(yán)重制約了想象力的施展。
針對(duì)引用辨赐,Rust提供了兩種類型:
- &:共享引用
- &mut:可變引用
在編譯階段优俘,Rust的行為是,同一作用域內(nèi)掀序,對(duì)于某一個(gè)對(duì)象的引用帆焕,只允許存在兩種情況:要么只有一個(gè)可變引用,要么同時(shí)存在多個(gè)共享引用不恭,共享引用不允許修改內(nèi)容叶雹,可變引用才有修改權(quán)限。
比如:
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;
編譯失敗浑娜,因?yàn)?code>person_ref屬于共享引用,并沒有修改權(quán)限式散。
再看一個(gè)簡(jiǎn)單的例子:
fn main() {
let x = 1;
let y = &x;
y = 2;
}
其中筋遭,
y=2;
報(bào)錯(cuò),因?yàn)?code>y屬于共享引用暴拄。
編譯階段漓滔,Rust borrow checker會(huì)對(duì)對(duì)象修改進(jìn)行檢查,一旦監(jiān)測(cè)到共享引用修改引用指向的內(nèi)容就會(huì)報(bào)錯(cuò)乖篷,這個(gè)檢查符合rust的設(shè)計(jì)原則响驴,但是對(duì)于開發(fā)者的要求卻顯得過于苛刻。
作為面向系統(tǒng)開發(fā)的語(yǔ)言撕蔼,大多數(shù)rust開發(fā)者很有可能是從C/C++系列遷移而來豁鲤,在此類語(yǔ)言中,獲得一個(gè)變量的引用鲸沮,然后對(duì)其進(jìn)行修改是一件非常正常的事情琳骡。
另一方面,由于 Rust 的 mutable特性讼溺, 一個(gè)結(jié)構(gòu)體中的字段楣号,要么全都是 immutable,要么全部是mutable怒坯,不支持針對(duì)部分字段進(jìn)行設(shè)置。比如剔猿,在一個(gè)struct中视译,可能只有個(gè)別的引用需要修改,而其他變量并不需要修改归敬,為了一個(gè)變量而將整個(gè)struct變?yōu)?code>&mut也是不合理的酷含。
作為語(yǔ)言規(guī)范鄙早,盡管Rust在設(shè)計(jì)范式上開誠(chéng)布公,也給了開發(fā)者明確的預(yù)期第美,但是與大多數(shù)人長(zhǎng)期形成的習(xí)慣進(jìn)行對(duì)抗并非什么明智之舉蝶锋。畢竟陆爽,弱小和無知不是生存的障礙什往,傲慢才是。
還好Rust的founder們并不是傲慢的人慌闭,為了解決這個(gè)現(xiàn)實(shí)問題别威,專門引入了內(nèi)部可變性。
所謂內(nèi)部可變性驴剔,簡(jiǎn)單理解省古,就是賦予共享引用修改的權(quán)限,由于這個(gè)“賦予”行為是明確指定的丧失,并未違反rust的設(shè)計(jì)原則豺妓。
內(nèi)部可變性引入了Cell與RefCell兩個(gè)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來實(shí)現(xiàn):
樣例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);
}
內(nèi)部可變性違背了rust關(guān)于共享引用的約定布讹,但是通過引入Cell與RefCell琳拭,這種走后門的行為是嚴(yán)格備案的,并沒有違背原則描验。
既然Cell與RefCell都能夠?qū)崿F(xiàn)內(nèi)部可變性白嘁,那這兩者之間有什么差異呢?
Cell與RefCell差異
首先來看定義:
struct Cell<T> {
value: UnsafeCell<T>,
}
struct RefCell<T: ?Sized> {
borrow: Cell<usize>,
value: UnsafeCell<T>,
}
RefCell相比Cell膘流,內(nèi)部維護(hù)了一個(gè)包裝對(duì)象的引用計(jì)數(shù)絮缅,當(dāng)通過RefCell.borrow
獲取一個(gè)共享引用時(shí),內(nèi)部引用計(jì)數(shù)加一呼股,當(dāng)獲取的引用離開作用域時(shí)耕魄,內(nèi)部引用計(jì)數(shù)減一,當(dāng)RefCell.borrow_mut
獲取一個(gè)可變引用時(shí)彭谁,首先檢測(cè)引用技數(shù)是否為 0屎开,如果為 0,正常返回马靠,如果不為 0奄抽,直接 panic,其實(shí)RefCell.borrow
時(shí)也會(huì)做類似的檢測(cè)甩鳄,當(dāng)已經(jīng)獲取了可變引用也是直接 panic逞度, 當(dāng)然為了避免 panic,我們可以用 RefCell.try_borrow
和RefCell.try_borrow_mut
來獲取一個(gè) Result 類型妙啃。
由于Cell并未引入引用計(jì)數(shù)档泽,所以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);
}
對(duì)于Cell而言俊戳,通過get
獲取到的是原有對(duì)象的拷貝,set
則使用新的對(duì)象替換原有老對(duì)象馆匿。RefCell<T>
沒有這個(gè)約束抑胎,它的操作都是通過返回可變指針完成。
由于實(shí)現(xiàn)機(jī)制上的差別渐北,Cell只能包裝Copy
類型阿逃,而RefCell能夠包裝任意類型,所以在不確定一個(gè)對(duì)象是否實(shí)現(xiàn)了Copy
時(shí)赃蛛,應(yīng)該選擇RefCell恃锉。
由于上述差異,RefCell更加常用呕臂,通常的做法是配合Rc破托,組成Rc<RefCell<T>>
。
限制
由于Cell與RefCell均未實(shí)現(xiàn)Sync
歧蒋,所以這兩種類型均只能用于單線程土砂。