深入研究Rust的內(nèi)部可變性- Cell是如何工作的?
在Rust中,我們從引用借用的規(guī)則中知道有不可變(共享)引用和可變(獨(dú)占)引用。
如果我們有一個(gè)共享引用,我們可以想要多少就有多少。這是因?yàn)檫@些引用不允許我們改變它們指向的值,所以同時(shí)有多個(gè)引用是可以的。
可變引用則不然,顧名思義,可變引用允許我們改變它們所指向的值。所以在這種情況下,對(duì)值有多個(gè)引用是不行的。例如,考慮兩個(gè)線程,其中每個(gè)線程都持有一個(gè)獨(dú)占引用并同時(shí)更改其值。線程運(yùn)行后的值應(yīng)該是什么?確切地說,這是一個(gè)未定義的行為!
那么為什么Rust允許我們擁有可共享的可變?nèi)萜髂兀窟@不是打破了Rust的借用規(guī)則嗎?這是因?yàn)檫@些容器有限制,允許以安全的方式使用它們,同時(shí)仍然提供允許可變的api。這就是為什么這些類型提供“內(nèi)部可變性”,當(dāng)使用它們時(shí),它們作為容器施加限制,在這些限制下,它們所持有的類型可以被安全地修改!
Cell 如何提供內(nèi)部可變性?
基本上,Cell通過確保沒有指向其保存的數(shù)據(jù)的指針并且在單線程環(huán)境中執(zhí)行來實(shí)現(xiàn)這一點(diǎn)。
有了這些限制,更改Cell中的數(shù)據(jù)是完全可以的。想想看,如果我們知道Cell中沒有指向數(shù)據(jù)的指針,并且它不是跨線程共享的,則可以保證我們對(duì)它具有獨(dú)占訪問權(quán)。
現(xiàn)在的問題是,Cell是如何施加這些約束的?Cell通過從不返回對(duì)其內(nèi)部數(shù)據(jù)的引用來實(shí)現(xiàn)這一點(diǎn),它總是返回?cái)?shù)據(jù)的副本。因此,這已經(jīng)告訴Cell適用于內(nèi)存開銷小的類型,例如整數(shù)。
此外,Cell沒有實(shí)現(xiàn)Sync,因此它不能在線程邊界之間共享。
Cell的構(gòu)建塊是UnsafeCell,這是Rust內(nèi)部可變性的構(gòu)建塊之一。UnsafeCell允許我們?cè)谌魏螘r(shí)候獲得一個(gè)原始的獨(dú)占指針,指向它所保存的數(shù)據(jù)。這當(dāng)然是一個(gè)不安全的操作,所以我們必須在unsafe{}塊中進(jìn)行操作。
Cell的一種可能的簡(jiǎn)化實(shí)現(xiàn)是:
use std::cell::UnsafeCell;
struct Cell<T> {
value: UnsafeCell<T>
}
// 禁止跨線程使用Cell
impl<T> !Sync for Cell<T> {}
impl<T> Cell<T> {
pub fn new(value: T) -> Self {
Cell { value: UnsafeCell::new(value) }
}
pub fn set(self, value: T) {
// 用一個(gè)新值覆蓋單元格所指向的值
unsafe { *self.value.get() = value }
}
pub fn get(&self) -> T where T: Copy {
// 返回Cell所指向的數(shù)據(jù)的副本
unsafe { *self.value.get() }
}
}
這里我們使用UnsafeCell來存儲(chǔ)Cell的數(shù)據(jù),不允許在線程之間共享此類型,最后,我們從不引用Cell中的數(shù)據(jù)。注意,get方法只適用于實(shí)現(xiàn)Copy的類型,并且返回內(nèi)部類型的副本。
在本文中,我們探討了Rust的Cell類型,我們了解到Cell通過對(duì)其持有的數(shù)據(jù)施加約束來允許內(nèi)部可變性。