[英]rust how to have multiple mutable references to a stack allocated object?
假設我們有這個 C 代碼:
typedef struct A { int i; } A;
typedef struct B { A* a; } B;
typedef struct C { A* a; } C;
int main(void)
{
A a = { .i = 42 };
B b = { .a = &a };
C c = { .a = &a };
}
在這種情況下,A 是堆棧分配的,B 和 C 指向 A 所在的堆棧分配 memory。
我需要在 rust 中做完全相同的事情,但每次我嘗試創建多個可變引用時都會抱怨。
不得不與語言對抗以完成如此基本的事情有點令人沮喪。
這可能不適用於您在評論中指出的具體情況,但在 Rust 中解決此問題的一般方法是使用RefCell
。 此類型允許您從&RefCell<T>
獲取&mut T
例如:
use std::cell::RefCell;
struct A(pub RefCell<i32>);
struct B<'a>(pub &'a RefCell<i32>);
fn main() {
let a = A(RefCell::new(0));
let b = B(&a.0);
let c = B(&a.0);
*b.0.borrow_mut() = 1;
println!("{}", c.0.borrow());
*c.0.borrow_mut() = 2;
println!("{}", b.0.borrow());
}
請注意, RefCell
以借用計數的形式存在開銷,這是在運行時強制執行 Rust 的別名規則所必需的(如果可變借用與任何其他借用同時存在,它將出現恐慌)。
如果底層類型是Copy
並且您不需要對內部值的引用,那么您可以使用Cell
,它沒有任何運行時開銷,因為每個操作都會完全檢索或替換包含的值:
use std::cell::Cell;
struct A(pub Cell<i32>);
struct B<'a>(pub &'a Cell<i32>);
fn main() {
let a = A(Cell::new(0));
let b = B(&a.0);
let c = B(&a.0);
b.0.set(1);
println!("{}", c.0.get());
c.0.set(2);
println!("{}", b.0.get());
}
請注意, Cell
是#[repr(transparent)]
,它在系統編程中特別有用,因為它允許在不同類型之間進行某些類型的零成本轉換。
不得不與語言對抗以完成如此基本的事情有點令人沮喪。
它不像你想象的那么基本。 Rust 的主要前提是零未定義行為,並且幾乎不可能同時擁有兩個可變引用同時維護該保證。 您將如何確保通過多線程不會意外獲得競爭條件? 這已經是可用於惡意手段的未定義行為。
學習 Rust 並不容易,如果您來自不同的語言,則尤其困難,因為許多編程范例根本不適用於 Rust。 但我可以向你保證,一旦你了解了如何以不同的方式構造代碼,它實際上將成為一件好事,因為 Rust 迫使程序員遠離有問題的模式,或者看起來不錯但需要再看一眼才能理解實際錯誤的模式跟他們。 C/C++ 錯誤通常是非常微妙的,並且是由一些奇怪的極端情況引起的,在 Rust 中編程一段時間后,確保這些極端情況根本不存在是非常值得的。
但是回到你的問題。
這里有兩個語言概念需要結合起來才能實現你想要做的事情。
這一次,借用檢查器強制您一次只有一個對特定片段數據的可變引用。 這意味着,如果您確實想從多個地方修改它,您將不得不利用一個稱為內部可變性的概念。 根據您的用例,有幾種方法可以創建內部可變性:
Cell
- 單線程,用於可以通過復制來替換的原始類型。 這是一個零成本的抽象。RefCell
- 單線程,用於需要可變引用而不是通過替換來更新的更復雜的類型。 檢查它是否已被借用的最小開銷。Atomic
- 多線程,用於原始類型。 在大多數情況下,零成本抽象(在 x86-64 上,直到 u64/i64 的所有內容都已經是開箱即用的原子,需要零開銷)Mutex
- 類似RefCell
,但用於多個線程。 由於活動的內部鎖管理,開銷更大。 因此,根據您的用例,您需要選擇正確的用例。 在您的情況下,如果您的數據確實是int
,我會使用 go 和Cell
或Atomic
。
其次,首先存在如何獲取對 object 的多個(不可變)引用的問題。
馬上,我想告訴你:不要過早地使用原始指針。 原始指針和unsafe
繞過借用檢查器並使 Rust 作為語言毫無意義。 99.9% 的問題在不使用原始指針的情況下運行良好且性能良好,因此僅在絕對沒有替代方案存在的情況下使用它們。
也就是說,共享數據的一般方法有以下三種:
&A
- 正常參考。 雖然引用存在,但引用的 object 無法移動或刪除。 所以這可能不是你想要的。Rc<A>
- 單線程引用計數器。 非常輕巧,所以不用擔心開銷。 訪問數據是零成本抽象,僅當您復制/刪除實際的Rc
object 時才會產生額外成本。 理論上移動Rc
object應該是免費的,因為這不會改變引用計數。Arc<A>
- 多線程引用計數器。 與Rc
一樣,實際訪問是零成本,但復制/刪除Arc
object 本身的成本比Rc
高一點。 移動Arc
object 理論上應該是免費的,因為這不會改變引用計數。因此,假設您有一個單線程程序並且問題與您提出的完全一樣,我會這樣做:
use std::{cell::Cell, rc::Rc};
struct A {
i: Cell<i32>,
}
struct B {
a: Rc<A>,
}
struct C {
a: Rc<A>,
}
fn main() {
let a = Rc::new(A { i: Cell::new(42) });
let b = B { a: Rc::clone(&a) };
let c = C { a: Rc::clone(&a) };
b.a.i.set(69);
c.a.i.set(c.a.i.get() + 2);
println!("{}", a.i.get());
}
71
但當然所有其他組合,如Rc
+ Atomic
、 Arc
+ Atomic
、 Arc
+ Mutex
等也是可行的。 這取決於您的用例。
如果您的b
和c
對象的壽命可證明比a
短(意思是,如果它們僅存在於幾行代碼並且不會移動到其他任何地方),那么當然使用引用而不是Rc
。 Rc
和直接引用之間最大的性能差異是Rc
內部的 object 存在於堆上,而不是棧上,因此它相當於在 C++ 中調用一次new
/ delete
。
因此,作為參考,如果您的數據共享允許 object 存在於堆棧中,就像在我們的示例中一樣,那么代碼將如下所示:
use std::cell::Cell;
struct A {
i: Cell<i32>,
}
struct B<'a> {
a: &'a A,
}
struct C<'a> {
a: &'a A,
}
fn main() {
let a = A { i: Cell::new(42) };
let b = B { a: &a };
let c = C { a: &a };
b.a.i.set(69);
c.a.i.set(c.a.i.get() + 2);
println!("{}", a.i.get());
}
71
請注意,引用是零成本的,而Cell
也是零成本的,因此此代碼將執行 100% 的操作,就像您使用原始指針一樣; 不同的是借用檢查器現在可以證明這不會導致未定義的行為。
為了證明這是多么的零成本,請查看上面示例的組件 output 。 編譯器設法將整個代碼優化為:
fn main() {
println!("{}", 71);
}
請注意,在您的 C 示例中,沒有什么可以阻止您將b
object 復制到其他地方,而a
離開 scope 並被破壞。 這將導致未定義的行為,並會被 Rust 中的借用檢查器阻止,這就是結構B
和C
攜帶生命周期'a
以跟蹤它們借用A
的事實的原因。
最后,我想談談unsafe
的代碼。 是的,如果您與C
接口或編寫需要直接訪問 memory 的低級驅動程序,那么unsafe
是絕對重要的。 也就是說,了解如何處理unsafe
以保持 Rust 的安全保證是很重要的。 否則,使用 Rust 真的沒有任何意義。 不要只是為了方便而使用unsafe
來簡單地否決借用檢查器,而是要確保由此產生的unsafe
使用是合理的。 在使用unsafe
關鍵字之前,請閱讀這篇關於健全性的文章。
我希望這能讓您了解在 Rust 中編程需要什么樣的思維,並希望它沒有嚇到您太多。 給它一個機會; 雖然學習曲線相當陡峭,特別是對於具有豐富其他語言先驗知識的程序員來說,但它可能是非常有益的。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.