如何解释Rust中对可变类型的不可变引用?

9

如果在我的解引用链中有任何不可变的引用,那么我似乎无法修改任何内容。一个例子:

fn main() {
    let mut x = 42;
    let y: &mut i32 = &mut x; // first layer
    let z: &&mut i32 = &y; // second layer
    **z = 100; // Attempt to change `x`, gives compiler error.

    println!("Value is: {}", z);
}

我遇到了编译器错误:
error[E0594]: cannot assign to `**z` which is behind a `&` reference
 --> src/main.rs:5:5
  |
4 |     let z: &&mut i32 = &y; // second layer
  |                        -- help: consider changing this to be a mutable reference: `&mut y`
5 |     **z = 100; // Attempt to change `x`, gives compiler error.
  |     ^^^^^^^^^ `z` is a `&` reference, so the data it refers to cannot be written

从某种意义上讲,这是有道理的,否则编译器将无法防止对同一变量有多个可变访问路径。

然而,当查看类型时,语义似乎是反直觉的:

  • 变量 y 的类型为 &mut i32,或者用简单的英语说,“一个可变整数的引用”。
  • 变量 z 的类型为 &&mut i32,或者用简单的英语说,“一个对可变整数的引用的不可变引用”。
  • 通过解引用 z 一次(即 *z),我将获得一个类型为 &mut i32 的东西,与 y 相同类型的东西。然而,再次对此进行解引用(即 **z)会使我得到一个类型为 i32 的东西,但我不能对该整数进行修改。

实质上,引用类型在某种程度上欺骗了我,因为它们并没有像它们所声称的那样真正地完成自己的任务。在这种情况下,我应该如何正确阅读引用类型,或者还有什么其他方法可以恢复对该概念的信心?

使用此示例进行测试:

fn main() {
    let mut x = 42;
    let y: &mut i32 = &mut x; // first layer
    let m: &&mut i32 = &y; // second layer
    let z: &&&mut i32 = &m; // third layer
    compiler_builtin_deref_first_layer(*z);
}

fn compiler_builtin_deref_first_layer(v: &&mut i32) {
    compiler_builtin_deref_second_layer(*v);
}

fn compiler_builtin_deref_second_layer(w: &mut i32) {
    println!("Value is: {}", w);
}

这最后两个函数的参数类型是正确的。如果我更改其中任何一个,编译器都会抱怨类型不匹配。然而,如果我按原样编译此示例,我会得到以下错误:
error[E0596]: cannot borrow `**v` as mutable, as it is behind a `&` reference

一些奇怪的事情发生了,对于compiler_builtin_deref_first_layer函数的调用似乎没问题,但是对于compiler_builtin_deref_second_layer的调用就不行了。编译器错误提到了**v,但我只看到了一个*v

1
通过对 z 进行一次解引用(即 *z),我将得到一个类型为 &mut i32 的东西。不,那将是一个可变的解引用,这不能在不可变引用上进行。你最多只能从那里获得一个 &i32 - E net4
1
这些是 DerefDerefMut。每个 trait 的文档都会解释在哪种情况下使用它们。 - E net4
2
@domin 不是的,根据上面链接的文档,类型 <&&mut i32 as Deref>::Target&mut i32。查看 Deref trait 的定义并不能真正帮助这里,因为相关步骤是内置于编译器中的,而不是标准库中的。 - Sven Marnach
3
有时候,将引用分为“排它引用”和“共享引用”,而不是“可变引用”和“不可变引用”,会有所帮助。因此,y 是指向整数的排它引用,而 z 是指向一个指向整数的排它引用的共享引用。但是,您不能对 z 进行解引用来获取类似于 y 的排它访问整数,因为这违反了 z 自身“共享性”的合同。 - trent
1
@domin 正如我之前所说,借用检查器是一个复杂的东西,但实际上并没有必要理解它的工作原理。您可以信任它来维护它应该维护的不变量。关于不同的错误的良好观察 - 在此显式添加类型确实会更改语义。没有类型时,let 绑定会将可变引用“移动”,而使用显式类型注释时,绑定会创建一个隐含的重新借用。这仅适用于可变引用,并且是一种相当微妙的区别。 - Sven Marnach
显示剩余10条评论
1个回答

14
实际上,在某种程度上,引用类型对我来说是有误导性的,因为它们并没有像它们所声称的那样做。在这种情况下,我应该如何正确地阅读引用类型,或者还有什么其他方法可以恢复对该概念的信心?
在Rust中正确阅读引用的方式是将其视为权限。
当对象没有被借用时,拥有对象的所有权意味着您可以对对象进行任何操作;创建、销毁、从一个地方移动到另一个地方。您是所有者,可以随心所欲,控制该对象的生命周期。
可变引用从所有者处借用对象。在可变引用存在期间,它授予对对象的独占访问权限。没有人可以读取、写入或执行其他任何操作。可变引用也可以称为独占引用或独占借用。您必须将对象的控制权归还给原始所有者,但同时,您可以随心所欲地使用它。
不可变引用或共享借用意味着您可以与其他人同时访问它。因此,您只能读取它,而没有人可以修改它,否则根据操作发生的确切顺序会产生未定义的结果。
可变(或独占)引用和不可变(或共享)引用都可以指向所拥有的对象,但这并不意味着您在通过引用引用它时拥有该对象。您可以使用对象的操作受到通过引用访问它的引用类型的限制。
因此,不要认为&&mut T引用是“对T的不可变引用的可变引用”,然后认为“我不能改变外部引用,但我应该能够改变内部引用”。
相反,将其视为“某人拥有一个T。他们已经提供了独占访问权限,因此现在有人有权修改T。但同时,那个人已经提供了对&mut T的共享访问权限,这意味着他们已经承诺在一段时间内不会对其进行修改,并且所有用户都可以使用共享引用来访问&mut T,包括解引用到底层的T,但只能进行与共享引用通常可以进行的读取而不是写入相关的事情。”
最后要记住的是,可变或不可变部分实际上并不是引用之间的根本区别。真正的区别在于独占和共享部分。在Rust中,您可以通过共享引用修改某些内容,只要有某种内部保护机制确保只有一个人可以同时这样做。有多种方法可以实现这一点,例如CellRefCellMutex
因此,&T&mut T提供的不是真正的不可变或可变访问,尽管它们被命名为这样,因为在缺乏任何库功能的情况下,这是它们在语言层面上提供的默认访问级别。但它们真正提供的是共享或独占访问权限,然后数据类型上的方法可以根据它们是否采用拥有值、独占引用或共享引用向调用者提供不同的功能。
因此,请将引用视为权限;决定您可以对其执行哪些操作的是您通过引用访问该对象。当您拥有所有权或独占引用时,分配独占或共享引用会暂时防止您在这些借用引用仍然存在时对该对象进行可变访问。

非常感谢您详细的回答!因此,多个堆叠引用仍应被视为指向相同基础拥有值(T)的引用,直接或间接地?如果是这样,那么为什么我可以采取 let foo = &mut &mut x;(其中 x:T),然后在一半的解引用之后改变中间引用,就像这样:*foo = &mut y;,其中 y 是另一个类型为 T 的值?这个操作与原始的 T 没有任何关系,所以一个引用必须是一个对象(带有所有者)本身... - domin
1
是的,引用是一个拥有者本身的对象。但是你获得的权限基于你到达它的路径。如果你通过一个&引用访问一个&mut引用,你只能对引用本身和它所引用的任何内容进行共享、只读访问。 - Brian Campbell
1
我应该澄清一下:多个堆叠的引用并不仅仅被读作指向基础值的引用。它们指向它们所指向的内容。但是您获得的权限是您到达它的路径中每个引用授予的权限的最小值。我会编辑我的答案以更清晰地表达。 - Brian Campbell
太棒了,感谢澄清。我刚刚利用那些知识试了一下,并得到了这个示例: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=c4d2880792180205d906fde034117238 你同意我的评论吗? - domin
1
@domin 是的,没错。借用检查器是静态工作的,虽然在理论上可能会对这个特定的代码片段进行静态分析,但如果您将它们传递到函数中,它将无法确定函数返回后foo所引用的是哪个引用(https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=fe87f5ed958e0be75618e8e6d35feb30)。因此,编译器应用保守规则;它确定`foo`可以引用`x`或`y`中的任何一个,因此只要`foo`存在,就不能对`x`或`y`进行其他引用。 - Brian Campbell
1
好的,这就是一个典型的集合并分析!非常感谢你的帮助! - domin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接