为什么借用临时账户是合法的?

36

我来自C++,对于这段Rust代码的合法性感到相当惊讶:

let x = &mut String::new();
x.push_str("Hello!");

C++中,你不能获取临时对象的地址,并且临时对象不会在它出现的表达式结束后继续存在。

Rust中临时对象能存活多久?因为x只是一个借用,所以字符串的所有权归谁?


3
在C++中,你不能获取临时变量的地址。我不懂C++,但这总是正确的吗?一个常量引用是否延长了临时变量的生命周期? - Shepmaster
@Shepmaster &xx 的地址,我认为这对于临时变量来说是无效的。我应该将其与创建对临时变量的引用进行比较,这确实是可能的,甚至可以扩展临时变量的寿命,因此总体行为实际上与 Rust 所做的非常相似。 - Sven Marnach
@SvenMarnach:在C++中,您可以完美地获取临时变量的地址,struct T { T* me() { return this; } };将返回T实例的地址,无论它是否是临时的。此外,C++允许将const引用或r-value引用绑定到临时变量,并且引用只是一个伪装成指针的小东西。 - Matthieu M.
1
@SvenMarnach:不用担心 :) 语法非常相似,效果也相似(因为引用就是指针),所以这似乎是一个非常自然的错误。只是某种程度上 Stroustrup 决定了一些事情是允许的,而其他事情则不允许,因为他有一种直觉感觉这样会容易出错...而缺乏统一性在回顾时可能更加令人困惑 :) - Matthieu M.
我针对常见的rust错误 error[E0716] error E0716: temporary value dropped while borrowed (rust) 提出了简明扼要的问题。它链接回这个问题。 - JamesThomasMoon
显示剩余2条评论
3个回答

38

为什么借用临时变量是合法的?

和在C++中不合法的原因一样,因为有人说应该这样做

在Rust中临时变量存活多久?由于x只是一个借用,那么字符串的所有者是谁?

参考资料显示

表达式的临时作用域是包含该表达式的最小范围之一,可以是以下几种情况之一:
  • 整个函数体。
  • 一个语句。
  • ifwhileloop表达式的主体。
  • if表达式的else块。
  • ifwhile表达式的条件表达式或match守卫。
  • match分支的表达式。
  • lazy boolean expression的第二个操作数。

本质上,您可以将您的代码视为:

let mut a_variable_you_cant_see = String::new();
let x = &mut a_variable_you_cant_see;
x.push_str("Hello!");

参见:


1
谢谢!但是对于这个特定的问题,https://doc.rust-lang.org/1.48.0/reference/destructors.html#temporary-lifetime-extension 这个链接会更好吗? - khuttun

12

来自Rust参考手册:

临时生命周期

在大多数表达式上下文中使用值表达式时,将创建一个临时的未命名内存位置,该位置初始化为该值,并且表达式将评估为该位置

这适用于此处,因为String::new()是一个值表达式,并且正好位于&mut下方,处于放置表达式上下文中。现在引用运算符只需要通过这个临时内存位置,因此它成为整个右侧(包括&mut)的值。

然而,当正在创建分配给let声明的临时值表达式时,临时值将使用封闭块的生命周期创建

由于它被分配给变量,因此它具有直到封闭块结束的生命周期。

这也回答了这个问题关于以下两种情况之间的区别:

let a = &String::from("abcdefg"); // ok!

并且

let a = String::from("abcdefg").as_str(); // compile error

在第二种变体中,临时变量被传递到as_str()中,因此它的生命周期在语句结束时结束。

1
在第二种变体中,临时变量被传递到as_str()函数中,因此它的生命周期在语句结束时结束。现在我明白了,谢谢! - neevek
这不是编译错误,对于C/C++用户来说,这更加令人困惑。 - arunanshub
1
@arunanshub 这确实是编译器错误。请查看 https://play.rust-lang.org/?version=stable&mode=release&edition=2018&gist=9d36935f2d46ef9337d09945116b4fe5 在您的代码中,每个 let a 声明都会遮蔽先前的声明。 - sateesh
@sateesh 抱歉,我没有注意到变量的使用情况。现在我明白了。谢谢。 - arunanshub

10
Rust的MIR为我们提供了一些关于临时变量性质的见解;考虑下面这个简化的案例:
fn main() {
    let foo = &String::new();
}

以及它所产生的MIR(标准评论已被我替换为我的):

fn main() -> () {
    let mut _0: ();
    scope 1 {
        let _1: &std::string::String; // the reference is declared
    }
    scope 2 {
    }
    let mut _2: std::string::String; // the owner is declared

    bb0: {                              
        StorageLive(_1); // the reference becomes applicable
        StorageLive(_2); // the owner becomes applicable
        _2 = const std::string::String::new() -> bb1; // the owner gets a value; go to basic block 1
    }

    bb1: {
        _1 = &_2; // the reference now points to the owner
        _0 = ();
        StorageDead(_1); // the reference is no longer applicable
        drop(_2) -> bb2; // the owner's value is dropped; go to basic block 2
    }

    bb2: {
        StorageDead(_2); // the owner is no longer applicable
        return;
    }
}

你可以看到在分配引用之前,“隐形”的所有者已经接收了一个值,并且预期在所有者之前丢弃引用。
但我不确定为什么似乎有一个无用的“scope 2”,而所有者没有放置在任何范围内;我怀疑MIR可能还没有完全准备好。

谢谢!这表明Shepmaster提供的等效代码不仅仅是类比,而且实际上就是正在发生的事情。中间表示总是倾向于包含许多看似无用的位,在后续阶段进行优化。我还喜欢在某个随机点分配的可变空元组。 - Sven Marnach
1
@SvenMarnach 如果我没有记错,_0 元组只是 main() 函数的返回值;虽然不是最有趣的信息,但也有其价值 :)。 - ljedrz

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