可变借用自动变为不可变的吗?

3

看起来,可变引用 u 在下面的代码中自动变为不可变引用

let v = &*u;

然后,uv都是不可变的借用引用,因此两者都是被允许的。

use std::ascii::AsciiExt;

fn show(a: &str) {
    println!("a={}", a);
}

fn main() {
    let mut t = String::new();
    t.push('s');
    let u = &mut t;
    u.make_ascii_uppercase(); // u is really mutable here
    let v = &*u; // u became immutable to allow this?
    show(u); // both u and v are now accessible!
    show(v);
}

输出:

a=S
a=S

如果我在可变借用之后尝试使用u,则会出现问题。
show(v);

编译器将会回忆起
let v = &*u;

这真的是不被允许的:

cannot borrow `*u` as mutable because it is also borrowed as immutable

这是一个bug还是真的有“当不再需要可变性时自动将可变借用转换为不可变借用”的原则?我正在使用Rust 1.13.0。

3个回答

8

可变引用可以被不可变借用,但这里并非如此。

当使用&创建一个引用时,你需要明确其可变性;除非你指定&mut,否则它将是一个不可变引用。


你的示例可以简化为:

use std::ascii::AsciiExt;

fn main() {
    let mut t = "s".to_string();
    let u = &mut t;
    u.make_ascii_uppercase();
    let v = &*u;

    let () = v;
}

最后一行的技巧是让编译器告诉我们(在错误消息中)v的类型。它报告如下:
error[E0308]: mismatched types
 --> <anon>:9:9
  |
9 |     let () = v;
  |         ^^ expected reference, found ()
  |
  = note: expected type `&std::string::String`
  = note:    found type `()`

这里有:
- `u`:一个不可变绑定,是对`t`的可变借用 - `v`:一个不可变绑定,是通过`u`对`t`进行的不可变重新借用
然而,如果我将`v`行更改为`let v = &mut *u;`,那么我会得到`expected type '&mut std::string::String'`,然后我们有:
- `u`:一个不可变绑定,是对`t`的可变借用 - `v`:一个不可变绑定,是通过`u`对`t`进行的可变重新借用
重借用是这里的重要概念,它与`&*u`和`&mut *u`有关。重借用允许从现有引用形成新引用:
- 重借用访问最初借用的变量 - 在重借用的生命周期内,从中形成的引用被借用
重借用规则相对简单,它们反映了借用规则:
- 如果您从不可变引用开始: - 您只能将其重新借用为不可变引用,并且可以使用多个并发不可变重新借用 - 如果您从可变引用开始: - 您可以将其作为可变引用进行重新借用,独占式地 - 或者您可以将其重新借用为不可变引用,并且可以使用多个并发不可变重新借用
有趣的是,重新借用的引用可以比其形成的引用更长寿。
fn main() {
    let mut t = "s".to_string();

    let v;
    {
        let u = &mut t;
        v = &mut *u;
    }

    v.make_ascii_uppercase();
    show(v);
}

这是为了确保函数可以返回引用,当然。

因此,编译器会跟踪到原始借用值的重新借用;但是,由于重新借用机制,它允许形成对该原始值的不可变引用,即使在作用域内存在可变引用...并且只需确保新的不可变引用的生命周期内,该可变引用无法使用。


当函数接受引用时,编译器会自动在调用点引入一个重新借用,并确定其可变性;这就是这里发生的情况:show:实际上是在函数调用期间形成一个新的不可变引用show(&*u)


我还是不理解。通过调用 _u.make_ascii_uppercase();_,我确信借用是可变的,否则编译器会抱怨。 - Roman Polach
1
我也不太确定。你是说尽管 u 是对 t 的一个 &mut 引用,但直到传递给使用它的函数之前,它实际上并不会借用 t 吗? - Chris Emerson
@ChrisEmerson:嗯...不是的。或者如果我这样做了,那肯定是意外的...难道我的表达有那么不清楚吗? - Matthieu M.
当我将u变为可变引用时,那么创建另一个引用(v)应该是不允许的。但是代码可以编译通过。这就是我不理解的地方。 - Roman Polach
@RomanPolach:我已经审查了这个答案,为了解释我对它的理解。我必须承认对我来说很难,因为我觉得这种行为很直观,从未真正尝试深入解释它是如何工作的,所以如果答案中仍有不清楚的部分,请告诉我。 - Matthieu M.
显示剩余6条评论

1

这很令人困惑,所以让我们做些实验。

你的代码已编译通过:

let mut t = String::new();
t.push('s');
let u = &mut t;
u.make_ascii_uppercase(); // u is really mutable here
let v = &*u; // u became immutable to allow this?
show(u); // both u and v are now accessible!
show(v);

如果我们将let v这一行改为什么会发生呢?
let v = &t;

错误[E0502]: 无法将t作为不可变引用进行借用,因为它也被作为可变引用进行了借用 --> :12:14
好的,这与之前不同。这告诉我,尽管&*u&t类型相同,但前者是从u中(子)借用的,而后者正在尝试重新借用t
让我们尝试一个不同的实验。将上一行放回,但现在添加一些新内容:
let v = &*u;   // the original reborrow
let w = u;     // Try to move out of `u`

错误[E0502]: 无法将 t 借用为不可变的,因为它也被作为可变的借用了

--> :12:14

啊哈!这证实了 v 真正从 u 借用而不是直接从 t 借用。

现在,在原始代码中,让我们在末尾添加一个通过 u 尝试进行的变异:

let mut t = String::new();
t.push('s');
let u = &mut t;
u.make_ascii_uppercase(); // u is really mutable here
let v = &*u; // u became immutable to allow this?
show(u); // both u and v are now accessible!
show(v);
u.make_ascii_uppercase();

现在我理解了:
错误[E0502]:无法将 *u 作为可变借用,因为它也被不可变地借用。
我认为这基本上解释了发生了什么:
- u 可变地借用了 t。这防止了直接访问 t。 - v 不可变地借用了 u。这意味着 u 仍然可以被不可变地使用,但不能被可变地使用或移出。 - 另一个关键点是,只有当到该项的完整路径为可变时,才能使用可变值。由于 v 存在时不能对 u 进行可变借用,因此也不能可变地使用 *u。(最后这一点有些含糊;欢迎进一步澄清...)

我的看法是,如果 v 是可变借用,则 u 本身也被借用,因此必须遵守与普通变量相同的规则:如果 v 是可变借用,则根本不能重新借用 u;如果 v 是不可变借用,则只能创建其他不可变借用。 - Matthieu M.
最令人困惑的是在绑定“v”时对“u”的副作用。在第一个代码示例中,编译器检查“&* u”后是否不可变地使用“u”,并允许获取可变引用“v”。在最后一个代码示例中,它知道“u”实际上被用作可变引用,所以禁止“&* u”... - Roman Polach

0
首先,u 在任何时候都不可变,因为它是用 let u 声明的,而不是 let mut u。你可以改变它所指向的 String 是因为它持有一个可变引用;make_ascii_uppercase() 修改了 t
同样地,v 也是不可变的(在 let v 中没有 mut),所以当你调用在不可变引用上工作的 show() 时,借用规则不会被违反 - 你可以同时执行多个不可变借用。

是的,通过可变和不可变,我总是指借用/引用,而不是变量绑定。我试图描述的问题是,我进行了可变借用(u),并且在某些情况下,该借用变为不可变借用(仅像不可变借用一样使用时)。 - Roman Polach

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