[英]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.