作者注:以下答案最初是为如何交织的作用域创建“数据竞争”?撰写的。
编译器允许在假定&mut
指针是排他(不具别名)的情况下优化它们。你的代码打破了这个假设。
问题中的示例太过简单,无法展示任何有趣的错误行为,但考虑将 ref_to_i_1
和 ref_to_i_2
传递给修改两者并然后执行某些操作的函数:
fn main() {
let mut i = 42;
let ref_to_i_1 = unsafe { &mut *(&mut i as *mut i32) };
let ref_to_i_2 = unsafe { &mut *(&mut i as *mut i32) };
foo(ref_to_i_1, ref_to_i_2);
}
fn foo(r1: &mut i32, r2: &mut i32) {
*r1 = 1;
*r2 = 2;
println!("{}", r1);
println!("{}", r2);
}
编译器可能会(也可能不会)决定取消交错访问
r1
和
r2
,因为它们不允许别名。
fn foo(r1: &mut i32, r2: &mut i32) {
*r1 = 1;
println!("{}", r1);
*r2 = 2;
println!("{}", r2);
}
它甚至可能会意识到println!
总是打印相同的值,并利用这一事实进一步重新排列foo
:
fn foo(r1: &mut i32, r2: &mut i32) {
println!("{}", 1);
println!("{}", 2);
*r1 = 1;
*r2 = 2;
}
编译器能够进行这种优化很好!(即使像
Doug所回答的那样,Rust目前还没有实现。)优化编译器非常棒,因为它们可以使用如上述的转换来使代码运行更快(例如通过更好地将代码通过CPU流水线处理,或者通过使编译器在后续处理中执行更激进的优化)。一切相等的情况下,每个人都喜欢他们的代码运行得更快,不是吗?
你可能会说“嗯,这不是一种有效的优化,因为它没有做同样的事情。”但你是错误的:&mut引用的整个
点 就是它们不是别名。没有办法使
r1
和
r2
别名而不违反规则†,这就使得这种优化是有效的。
你可能还认为这只是出现在更复杂的代码中的问题,因此编译器应该允许简单的示例。但请记住,这些转换是长时间的多步优化过程的一部分。重要的是要在所有地方遵守
&mut
引用的属性,这样编译器就可以对
一个代码段进行轻微的优化,而不需要理解
所有代码。
还要考虑一件事:你作为程序员的工作是选择和应用适合你的问题的类型;偶尔请求编译器例外
&mut
别名规则就等于请它替你完成工作。
如果你想要共享可变性并放弃这些优化,那很简单:不使用
&mut
。在这个示例中,你可以像注释中提到的那样使用
&Cell<i32>
而不是
&mut i32
。
fn main() {
let mut i = std::cell::Cell::new(42);
let ref_to_i_1 = &i;
let ref_to_i_2 = &i;
foo(ref_to_i_1, ref_to_i_2);
}
fn foo(r1: &Cell<i32>, r2: &Cell<i32>) {
r1.set(1);
r2.set(2);
println!("{}", r1.get());
println!("{}", r2.get());
}
在
std::cell
中的类型提供了"内部可变性",这是术语,表示"不允许某些优化,因为
&
引用可能会改变事物状态"。它们并不总是像使用
&mut
那样方便,但这是因为使用它们会给你更多的灵活性来编写以上代码。
另请参阅
† 请注意,仅使用unsafe
本身并不算"违反规则"。为了让您的代码具有定义的行为,即使在使用unsafe
时,&mut
引用也不能被别名所引用。
&Cell<T>
,它与您想要的语义非常接近。这会排除一些编译器优化,不幸的是,编译器缺乏一些语法糖来方便地处理单元格。 - CodesInChaos