为什么Rust允许对可变变量进行不可变引用?

6
我正在阅读 Rust 书籍(第四章),我很惊讶像这样的代码可以编译
fn main() {
    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{}, {}", r1, r2);

    // this line silences the warning: 'variable does not need to be mutable'
    s.push_str(" world");
}

Rust为什么允许对可变变量进行不可变引用?这似乎会削弱安全性保证。如果我有一个可变变量,并将不可变引用传递给某些线程,那些线程将假设该值不会改变,但我可以通过原始变量来改变该值。

虽然我还没有涉及线程,但我发现这很奇怪,在这种情况下与C ++无异:

void doNotChangeMyString(const std::string& myConstString) {
  // ... assume that myConstString cannot change and use it on a thread
  // return immediately even though some worker thread is still
  // using myConstString
}

void main() {
    std::string s = "hello" // not const!
    doNotChangeMyString(s);
    s = "world"; // oops
}

编辑:我修复了Rust代码以便其可以编译。请重新考虑给我负评和关闭投票。被接受的答案解释了一个概念,我没有从Rust Book的借用章节中理解,这对我非常有帮助,也可能帮助其他正在学习Rust的人。


2
尝试真正编写你提示的代码。你会发现它无法编译。 - Denys Séguret
1
你是指 Rust 的版本吗?那可能就是答案了,也就是说我还没有在书中继续深入学习到那部分。 - Matthew James Briggs
1
只是为了明确,你实际发布的 Rust 代码也无法编译,因此你认为 Rust 允许这样做的假设是错误的。这就是为什么在书中它被印上了粉色的背景和困惑的螃蟹:为了说明这是一段不能编译的代码,以便进行说明。 - trent
1
我发布的 Rust 代码中的可变借用并不是为了阐明问题所必需的,你是对的,它导致了编译错误。我已经从示例代码中删除了可变借用,这应该会改善问题。@Optimistic Peach 解释的我不知道的概念是,如果原始变量已被修改,编译器将阻止我使用不可变引用。我认为这在《Rust Book》第4章中没有解释清楚。 - Matthew James Briggs
1
编译器会阻止我使用不可变引用,如果原始变量已经被改变,但这并不完全正确。非词法生命周期允许对象的生命周期被缩短,直到其最后一次使用,这与如果所指向的值被改变就不能使用它是不同的。当您改变值时,编译器只是丢弃共享引用。编译器确实防止在存在活动(未隐式丢弃)共享引用的情况下对值进行更改。 - Optimistic Peach
1个回答

4
一个项目的可变性在 Rust 中实际上是变量名的一部分。以这段代码为例:
let mut foo = String::new();
let foo = foo;
let mut foo = foo;

foo 突然变为不可变类型,但这并不意味着前两个 foo 不存在。

另一方面,可变引用 与对象的生命周期绑定,在其自身的生命周期内存在,并禁止通过引用之外的方式访问原始对象。

let mut my_string = String::new();
my_string.push_str("This is ok! ");
let foo: &mut String = &mut my_string;
foo.push_str("This goes through the mutable reference, and is therefore ok! ");
my_string.push_str("This is not ok, and will not compile because `foo` still exists");
println!("We use foo here because of non lexical lifetimes: {:?}", foo);

第二次调用my_string.push_str不会通过编译,因为在这种情况下可以(保证)在之后使用foo

您的具体问题类似于以下内容,但您甚至不需要多线程来测试此功能:
fn immutably_use_value(x: &str) {
    println!("{:?}", x);
}

let mut foo = String::new();
let bar = &foo; //This now has immutable access to the mutable object.
let baz = &foo; //Two points are allowed to observe a value at the same time. (Ignoring `Sync`)
immutably_use_value(bar); //Ok, we can observe it immutably
foo.push_str("Hello world!"); //This would be ok... but we use the immutable references later!
immutably_use_value(baz);
这段代码无法编译通过。 如果你能给生命周期加上注释,它们看起来会类似于这样:
let mut foo = String::new();  //Has lifetime 'foo
let bar: &'foo String = &foo; //Has lifetime 'bar: 'foo
let baz: &'foo String = &foo; //Has lifetime 'baz: 'foo
//On the other hand:
let mut foo = String::new();          //Has lifetime 'foo
let bar: &'foo mut String = &mut foo; //Has lifetime 'bar: mut 'foo
let baz: &'foo mut String = &mut foo; //Error, we cannot have overlapping mutable borrows for the same object!

一些额外的注意事项:
  • Due to NLL (Non Lexical Lifetimes), the following code will compile:

    let mut foo = String::new();
    let bar = &foo;
    foo.push_str("Abc");
    

    Because bar is not used after the mutable use of foo.

  • You mention threading, which has its own constraints and traits involved:

    The Send trait will allow you to give ownership of a variable across a thread.

    The Sync trait will allow you to share a reference to a variable across a thread. This includes mutable references, as long as the original thread does not use the object for the duration of the borrow.

    A few examples:

    • Type T is Send + Sync, it can be sent across threads and be shared between them
    • Type T is !Send + Sync, it can be shared across threads, but not sent between them. An example is a window handle that can only be destroyed on the original thread.
    • Type T is Send + !Sync, it can be sent across threads, but not shared between them. An example is RefCell, which will can only use its runtime borrow-checking on a single thread due to it not using atomics (Multithreading safe components).
    • Type T is !Send + !Sync, it can only live on the thread it was created on. An example is Rc, which cannot send a copy of itself across threads because it cannot count references atomically (Look at Arc to do that) and since it carries no lifetimes to force a single copy of itself to exist when sending across a thread boundary, it therefore cannot be sent across threads.
  • I use &str instead of &String in my third example, this is because String: Deref<str> (You may need to scroll down to see it), and therefore anywhere I need a &str I can chuck a &String in because the compiler will autoderef.

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