【问题标题】:Why Does Rust Allow an Immutable Reference to a Mutable Variable?为什么 Rust 允许对可变变量进行不可变引用?
【发布时间】:2019-10-27 16:12:28
【问题描述】:

我正在编写 Rust Book(第 4 章),我很惊讶像这样的代码 compiles:

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 方面处于同一点的人。

【问题讨论】:

  • 尝试真正编写您所暗示的代码。你会发现你无法编译它。
  • 你是说 Rust 版本?这可能是答案,即我在书中还没有走得足够远。
  • 为了清楚起见,您实际发布的 Rust 代码也无法编译,因此您假设 Rust 允许它是错误的。这就是为什么在本书中它被印成粉红色背景和一只迷惑的螃蟹:为了说明这是无法编译的代码
  • 我发布的 Rust 代码中的可变借用不是说明问题所必需的,你是对的,这是一个编译错误。我从示例代码中删除了可变借用,这应该可以改善问题。 @Optimistic Peach 解释的我不知道的概念是,如果原始变量已发生突变,编译器将阻止我使用不可变引用。我不认为这在 The Rust Book 第 4 章中有解释。
  • “如果原始变量已发生变异,编译器将阻止我使用不可变引用”,不完全是,您读错了。 NLL(非词法生命周期)允许缩短对象的生命周期,直到其最后一次使用不同于,如果它指向的值发生突变,则不允许使用它。当您改变值时,编译器会简单地删除共享引用。编译器阻止的是当存在对它的实时(非隐式删除)共享引用时值的突变

标签: rust


【解决方案1】:

项目的可变性本质上是 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);

This does not compile. 如果你可以注释生命周期,它们看起来会类似于这样:

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!

一些额外的说明:

  • 由于 NLL (Non Lexical Lifetimes),以下代码将编译:

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

    因为barfoo的可变使用后没有被使用。

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

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

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

    几个例子:

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-07
    • 1970-01-01
    • 1970-01-01
    • 2016-10-19
    • 1970-01-01
    相关资源
    最近更新 更多