我有一个值,我想在我自己的类型中存储该值以及对该值内部内容的引用:
struct Thing {
count: u32,
}
struct Combined<'a>(Thing, &'a u32);
fn make_combined<'a>() -> Combined<'a> {
let thing = Thing { count: 42 };
Combined(thing, &thing.count)
}
有时候,我有一个值,我想在同一个结构中存储该值和对该值的引用:
struct Combined<'a>(Thing, &'a Thing);
fn make_combined<'a>() -> Combined<'a> {
let thing = Thing::new();
Combined(thing, &thing)
}
有时,我甚至没有参考该值,我得到同样的错误:
struct Combined<'a>(Parent, Child<'a>);
fn make_combined<'a>() -> Combined<'a> {
let parent = Parent::new();
let child = parent.child();
Combined(parent, child)
}
在每种情况下,我都会收到错误,其中一个值“活不够长” . 这个错误是什么意思?
2 回答
我们来看看a simple implementation of this:
这将失败并显示错误:
要完全理解此错误,您必须考虑如何在内存中表示值以及移动这些值时会发生什么 . 让我们用一些假设的内存地址注释
Combined::new
,它们显示值的位置:child
会发生什么?如果只是像parent
那样移动了值,那么它将引用不再保证其中包含有效值的内存 . 允许任何其他代码在内存地址0x1000处存储值 . 假设它是一个整数,访问该内存可能会导致崩溃和/或安全漏洞,并且是Rust阻止的主要错误类别之一 .这正是生命周期所阻止的问题 . 生命周期是一些元数据,允许您和编译器知道值在 current memory location 处有效的时间 . 这是Rust新手们犯的一个常见错误 . Rust生命周期不是创建对象和销毁对象之间的时间段!
作为类比,以这种方式思考:在一个人的生活中,他们将居住在许多不同的地方,每个地点都有不同的地址 . Rust生命周期与您当前居住的地址有关,而与您将来何时死亡无关(尽管死亡也会改变您的地址) . 每次移动它都是相关的,因为您的地址不再有效 .
同样重要的是要注意,生命周期不会改变你的代码;您的代码控制着生命周期,您的生命周期不会控制代码 . 精辟的说法是"lifetimes are descriptive, not prescriptive" .
让我们使用一些行号注释
Combined::new
,我们将使用这些行号来突出生命周期:parent
的具体生命周期为1到4,包括(我将表示为[1,4]
) .child
的具体生命周期为[2,4]
,返回值的具体生命周期为[4,5]
. 可以使具体的生命周期从零开始 - 这将表示函数参数的生命周期或块外部存在的东西 .请注意,
child
本身的生命周期是[2,4]
,但它是 refers to 一个生命周期为[1,4]
的值 . 只要引用值在引用值之前变为无效,这就没问题 . 当我们尝试从块返回child
时会发生此问题 . 这将超过其自然长度 .这个新知识应该解释前两个例子 . 第三个需要查看
Parent::child
的实现 . 机会是,它看起来像这样:这使用生命周期省略来避免编写显式的通用生命周期参数 . 它相当于:
在这两种情况下,该方法都会返回
Child
结构,该结构已使用self
的具体生命周期进行参数化 . 换句话说,Child
实例包含对创建它的Parent
的引用,因此不能比Parent
实例更长寿 .这也让我们认识到我们的创建功能确实存在问题:
虽然您更有可能看到以不同形式编写的内容:
在这两种情况下,都没有通过参数提供生命周期参数 . 这意味着
Combined
将被参数化的生命周期不受任何约束 - 它可以是调用者想要的任何东西 . 这是荒谬的,因为调用者可以指定'static
生命周期,并且无法满足该条件 .如何解决?
最简单和最推荐的解决方案是不要试图将这些项目放在同一个结构中 . 通过这样做,您的结构嵌套将模仿代码的生命周期 . 将拥有数据的类型放在一起放在一个结构中,然后提供允许您获取引用的方法或根据需要包含引用的对象 .
有一种特殊情况,即终身跟踪过于热心:当您在堆上放置某些东西时 . 例如,当您使用
Box<T>
时会发生这种情况 . 在这种情况下,移动的结构包含指向堆的指针 . 指向的值将保持稳定,但指针本身的地址将移动 . 在实践中,这并不重要,因为你总是按照指针 .rental crate或owning_ref crate是表示这种情况的方法,但它们要求基地址永远不会移动 . 这排除了变异向量,这可能导致重新分配和移动堆分配的值 .
租赁解决问题的例子:
Is there an owned version of String::chars?
Returning a RWLockReadGuard independently from a method
How can I return an iterator over a locked struct member in Rust?
How to return a reference to a sub-value of a value that is under a mutex?
How do I store a result using Serde Zero-copy deserialization of a Futures-enabled Hyper Chunk?
How to store a reference without having to deal with lifetimes?
更多信息
虽然理论上可以这样做,但这样做会带来大量的复杂性和开销 . 每次移动对象时,编译器都需要插入代码来“修复”引用 . 这意味着复制一个结构不再是一个非常便宜的操作,只是移动一些位 . 它甚至可能意味着像这样的代码很昂贵,这取决于假设的优化器有多好:
程序员不是强迫每一次移动都发生这种情况,而是通过创建仅在调用它们时才会采用适当引用的方法来选择何时发生 .
具有对自身的引用的类型
有一个特定情况,您可以创建一个引用自身的类型 . 你需要使用像
Option
之类的东西来分两步:从某种意义上说,这确实有效,但创造的 Value 受到严格限制 - 永远无法移动 . 值得注意的是,这意味着它不能从函数返回或通过值传递给任何东西 . 构造函数显示与上述生命周期相同的问题:
Pin怎么样?
Pin,在Rust 1.33中稳定,有in the module documentation:
它's important to note that 2862079 doesn' t必然意味着使用引用 . 事实上,example of a self-referential struct具体说(强调我的):
自Rust 1.0以来,已存在使用原始指针进行此行为的能力 . 事实上,拥有参考和租赁使用引擎盖下的原始指针 .
Pin
添加到表中的唯一方法是声明给定值保证不移动的常用方法 .也可以看看:
导致非常相似的编译器消息的稍微不同的问题是对象生存期依赖性,而不是存储显式引用 . 一个例子是ssh2库 . 在开发比测试项目更大的东西时,很有可能尝试将从该会话中获得的
Session
和Channel
并排放入结构中,从而隐藏用户的实现细节 . 但是,请注意Channel定义在其类型注释中具有'sess
生存期,而Session则没有 .这会导致与生命周期相关的类似编译器错误 .
以一种非常简单的方式解决它的一种方法是在调用者中声明
Session
外部,然后使用生命周期在结构中注释引用,类似于在封装SFTP时讨论相同问题的答案 . 这看起来并不优雅,可能并不总是适用 - 因为现在你有两个实体要处理,而不是你想要的一个!结果是rental crate或其他答案的owning_ref crate也是这个问题的解决方案 . 让我们考虑一下owning_ref,它具有用于此目的的特殊对象:OwningHandle . 为了避免底层对象移动,我们使用
Box
在堆上分配它,这为我们提供了以下可能的解决方案:这段代码的结果是我们不能再使用
Session
,而是存储它与我们将使用的Channel
一起 . 因为OwningHandle
对象解引用Box
,它取消引用到Channel
,当它存储在结构中时,我们将其命名为 . NOTE: 这只是我的理解 . 我怀疑这可能不正确,因为它似乎非常接近discussion of OwningHandle unsafety .这里有一个奇怪的细节是
Session
逻辑上与TcpStream
具有类似的关系,因为Channel
必须Session
,但是它的所有权没有被采用,并且没有类型注释这样做 . 相反,由用户负责处理这个问题,因为handshake方法的文档说:因此,使用
TcpStream
,完全取决于程序员,以确保代码的正确性 . 使用OwningHandle
,使用unsafe {}
块绘制对"dangerous magic"发生位置的注意 .关于这个问题的进一步和更高层次的讨论在这个Rust User's Forum thread中 - 其中包括一个不同的例子及其使用租赁箱的解决方案,它不包含不安全的块 .