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

Mat*_*ggs 6 rust

我正在编写《Rust Book》(第 4 章),令我惊讶的是,这样的代码可以编译

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");
}
Run Code Online (Sandbox Code Playgroud)

为什么 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
}
Run Code Online (Sandbox Code Playgroud)

编辑:我修复了 Rust 代码以便它可以编译。请重新考虑否决票和接近票。接受的答案解释了我从 Rust Book 的借用章节中没有得到的概念,对我非常有帮助,并且可以帮助其他正在学习 Rust 的人。

Opt*_*ach 4

项目的可变性本质上是 Rust 中变量名称的一部分。以这段代码为例:

let mut foo = String::new();
let foo = foo;
let mut foo = foo;
Run Code Online (Sandbox Code Playgroud)

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);
Run Code Online (Sandbox Code Playgroud)

第二次调用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);
Run Code Online (Sandbox Code Playgroud)

这不能编译。如果您可以注释生命周期,它们看起来会类似于以下内容:

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!
Run Code Online (Sandbox Code Playgroud)

一些额外的注意事项:

  • 由于 NLL(非词法生命周期),以下代码将编译:

    let mut foo = String::new();
    let bar = &foo;
    foo.push_str("Abc");
    
    Run Code Online (Sandbox Code Playgroud)

    因为bar在可变使用之后就不再使用了foo

  • 您提到了线程,它有自己的约束和特征:

    Send特征将允许您在线程中授予变量的所有权。

    Sync特征将允许您跨线程共享对变量的引用。这包括可变引用,只要原始线程在借用期间不使用该对象即可。

    举几个例子:

    • 类型TSend + Sync,它可以跨线程发送并在线程之间共享
    • 类型T!Send + Sync,可以跨线程共享,但不能在线程之间发送。一个例子是只能在原始线程上销毁的窗口句柄。
    • 类型TSend + !Sync,可以跨线程发送,但不能在线程之间共享。一个例子是RefCell,由于它不使用原子(多线程安全组件),因此只能在单个线程上使用运行时借用检查。
    • 类型T!Send + !Sync,它只能存在于创建它的线程上。一个例子是Rc,它不能跨线程发送自身的副本,因为它不能自动计数引用(请查看执行Arc此操作),并且由于它没有生命周期来强制在跨线程边界发送时存在自身的单个副本,因此它不能跨线程发送。
  • 我在第三个示例中使用&str而不是&String,这是因为String: Deref<str>(您可能需要向下滚动才能看到它),因此在任何需要 a 的地方&str我都可以插入 a &String,因为编译器会自动引用。