为什么在Rust中不能返回一个临时变量的引用?下面是一个激励的例子:
fn what<'cellref, 'cell: 'cellref>(x: &f64) -> &'cellref CellValue<'cell> {
&CellValue::Number(*x)
}
字符串
有人可能会认为编译器可以推断出只要引用存在,对象就需要存活,因此保持对象存活直到引用被删除。
按照要求,这里是一个MRE。它说明了一个简单的问题:假设一个API需要一个&T
。您将T
存储在哪里,以便它与所需的&T
一样长?
pub enum CellValue<'a> {
String(&'a String),
Number(f64),
Boolean(bool),
// ... many more types ...
}
pub enum Array<'a> {
ArrayGeneric(ndarray::Array2<CellValue<'a>>),
ArrayF64(ndarray::Array2<f64>),
ArrayString(ndarray::Array2<f64>),
// .. many more types ...
}
impl Array<'a> {
pub fn generic_iter() -> Iterator<Item = &CellValue> {
// ?
}
}
型
与许多rust用例一样,目标是零成本的抽象。
作为动机,假设您有一个excel电子表格,并且您正在定义一个可以在excel函数之间传递的单元格“数组”。有时,这将是电子表格中的一个值范围。在其他时候,您可能已经应用了转换,如ABS(...)
,现在您有一个单元格范围的绝对值数组。在前一种情况下,你只需要存储完整的电子表格和一些usize
来定位范围。在后一种情况下,你需要存储所有的值。值可以是字符串,数字或其他值,所以你需要一个通用的数组来处理混合值,或者可能是一个单一类型的数组,只处理f64,只有字符串等。
2条答案
按热度按时间nom7f22z1#
编译器是否能推断出这一点是无关紧要的,因为没有编译器可以实现这个请求的机制。
请记住,Rust没有(内置)垃圾收集器,所以编译器不能只是发出指令在堆上分配枚举值,让GC来处理它。它需要一个地方来存储值,该值至少与
'cellref
一样长,但也不会超过'cell
。它不能制造这样的位置;你必须给予一个(例如以&'x mut CellValue<'cell>
的形式,其中'x: 'cellref
)。或者,你可以只返回CellValue<'cell>
而不是返回一个引用。问题中的代码类型可以用于纯常量值,因为编译器 * 确实 * 有一个位置来放置这些值:它可以将值发送到二进制文件的文本部分并返回一个引用。事实上,这正是字符串字面量所发生的事情!这里的问题是
x
不是编译时常量,而是一个引用,其referent直到运行时才知道。因此,编译器无法构建CellValue
的'static
示例。与此代码相比,它确实可以编译:
字符串
有一个解决方案:你可以泄漏一个
Box
。这里明显的缺点是保存CellValue
的内存被永久泄漏,并且在程序终止之前无法回收。型
cbjzeqam2#
这里有两个问题,我将尝试回答它们:
1.问题中的功能违反了哪些特定的 rust eclipse 规则?
1.为什么会有这些规则?
1.规则被打破
根据语言的规则,当函数返回时,引用的临时对象将被销毁。
在函数体的最后一个表达式中创建的临时变量将在绑定到函数体中的任何命名变量之后被丢弃。它们的丢弃范围是整个函数,因为没有更小的封闭临时范围。
考虑简单(无效)函数:
字符串
函数体的最终表达式是
&String::from("Hello")
。该值由函数调用String::from("Hello")
创建的临时对象拥有。函数的块在概念上被 Package 在一个绑定参数模式的块中,然后返回函数块的值。
这两个规则冲突。值不属于返回的引用(因为引用不拥有值),因此在函数返回时被销毁。因此,该引用无效,因为它所指向的值不再存在。
您无法使用存留期来避开这个限制,因为函数的所有存留期参数都会比函数的存留期长。您可以这样做:
型
但不是这个:
型
因为
'a
引用的作用域超过了函数的寿命。第一个示例将其值存储在名为'a
的作用域中;第二个示例将其值存储在函数体的匿名作用域中,该作用域在函数返回时终止。2.规则为何存在
(This部分是非规范性的:编译器不 * 必须 * 按照这里描述的方式工作,但它确实可以,而且很可能永远都可以。)
Rust运行时(它有一个,C!也有)使用调用堆栈来存储函数的局部变量。当函数被调用时,它会收到一个包含其参数和空间的堆栈帧来存储其局部变量;当函数返回时,它的堆栈帧会被销毁。这是大多数非Lisp(可以使用continuation passing)的编程语言所使用的模型。
因此,如果您尝试返回临时值,则会要求编译器生成代码,该代码返回一个已销毁的堆栈帧中的地址,下次调用函数时,该堆栈帧将被销毁。
编译器 * 可以 * 重新计算变量(比如Python),或者进行转义分析(比如Go语言),或者使用其他技术来保持值的活性,直到不再需要为止。但是语言被设计得尽可能的明确,并且可以在大量的环境中运行。值必须存在于某个地方;如果生成它的堆栈帧不再存在,则必须将它存储在其他位置,否则它也会消失。