需要澄清 Rust Nomicon 中关于 `Box`、`Vec` 和其他集合的 (协) 变性部分。

9
Rust Nomicon的一整个章节都在讲协变性,我对大部分内容都比较理解,只是有一个小问题与Box和Vec相关。
引用如下: “ Box和Vec是非常有趣的情况,因为它们是协变的,但是你肯定可以在它们里面存储值!这就是Rust变得非常聪明的地方:它们可以是协变的,因为你只能通过可变引用将值存储到其中!可变引用使整个类型不变,从而防止您将短寿命类型传递给它们。”
我的困惑在于以下一行: “ 它们可以是协变的,因为你只能通过可变引用将值存储到其中!”
我的第一个问题是我稍微有些困惑于可变引用所指代的对象是什么。 它是指Box / Vec的可变引用吗?
如果是这样,那么我只能通过可变引用将值存储在它们中,这如何证明它们是(协/反)变的合理性呢? 我了解(协/反)变性以及其对Box ,Vec 等的好处,但我很难看出仅能通过可变引用存储值的事实与(协/反)变性之间的联系。
此外,当我们初始化一个Box时,不是将值移入Box而不涉及可变引用吗?这是否与声明中指出的“只能通过可变引用存储值”不符?
最后,在什么情况下使用“可变引用”借用?他们是指当您调用修改Box或Vec的方法时,会隐式地获取&mut self吗?那可变引用是指所提到的可变引用吗?

1
很有趣,我今天刚看过那个页面,也没有真正理解那部分。我的问题是,&mut Box<T> 对于 T 是不变的这一事实实际上防止了什么?例如,这段代码Box<&'a str> 中的 &'a str 替换为 &'static str,它可以正常工作,但似乎这种情况应该被禁止,因为 &mut T 对于 T 是不变的。 - Michael Hewson
4个回答

2
我认为这一部分需要改进以使其更加清晰易懂。
引用块中的可变引用指的是什么我有点困惑。它是对 Box / Vec 的可变引用吗?
不是。这意味着,如果您要将值存储在现有的 Box 中,则必须通过对数据进行可变引用来实现,例如使用 Box::borrow_mut()。
本节的主要思想是,当存在其他引用时,您无法修改 Box 的内容。这是因为 Box 拥有其内容。为了更改 Box 的内容,您必须通过获取新的可变引用来完成。
这意味着即使您使用短暂的值覆盖了内容,也没有关系,因为没有人可以使用旧值。借用检查器不会允许它。
这与函数参数不同,因为函数具有可以实际处理其参数的代码块。在 Box 或 Vec 的情况下,您必须先通过可变借用获取内容,然后才能对其进行任何操作。

谢谢您的回复。我已经思考了您的答案很长时间,我认为我已经理解了您的意思。希望您能检查一下我的理解是否正确。 - L.Y. Sim
在 nomicon 中的“Box”和“Vec”部分上面的段落中,解释了“&mut T”不能在“T”上是协变的,因为“T”有一个所有者,应该最终“控制”“T”的生命周期。但是如果我们允许“&mut T”在“T”上是协变的,那么我们就可以在需要更长寿命值的地方使用短寿命值,就像 Nomicon 的示例代码中的“overwrite”函数一样。不变性保护我们免受非法操作的侵害。 - L.Y. Sim
允许 Box<E>E 协变并不会引入存储值被替换为非法值的风险,因为修改 Box 中存储的值的唯一方法是首先获取对该值的可变引用(例如使用 .borrow_mut() 获取 &mut E)。因此,在我们打算在 Box 中修改值的每个可能的上下文中,我们都必须通过不变的 E 来进行 &mut E,这意味着我们无法执行非法操作,这也意味着 Box<E> 在突变上下文中实际上是不变的。 - L.Y. Sim
1
@LYSim 我认为你想得太多了。Nomicon中的文本写得不好,如果你已经明白为什么TBox <T>中是协变的,我认为你不必担心理解它。你说的基本上是正确的,尽管我不确定你最后一句话的意思。任何Box <T>都可以通过borrow_mut()进行突变,我们在这里讨论的方差是_type constructor_的参数。整个讨论的目的是要表明允许这种方差是安全的。 - Peter Hall
谢谢 @Peter Hall,我现在对这个问题的理解更加深入了。如果我没有收到其他的回答,我会联系那些撰写文档的人,看他们能否改进这一部分。 - L.Y. Sim

1

来自nomicom

BoxVec是有趣的情况,因为它们是变体类型,但你肯定可以在它们里面存储值!这就是Rust变得非常聪明的地方:它们可以是变体类型,因为你只能通过可变引用将值存储在其中!可变引用使整个类型不变,并因此防止您将短暂类型走私到其中。

考虑Vec方法添加一个值:

pub fn push(&'a mut self, value: T)

自身类型是&'a mut Vec<T>,我理解这是nomicom所说的可变引用,因此对于Vec情况下实例化,上述短语的最后一句话变为:

类型&'a mut Vec<T>是不变的,因此防止您将短暂的类型走私到Vec<T>中。

Box也同样适用这种推理。

换句话说:VecBox包含的值始终比它们的容器存在时间更长,因为只能通过可变引用将值存储在其中,尽管VecBox是协变的。

考虑以下片段:

fn main() {
    let mut v: Vec<&String> = Vec::new();

    {
        let mut a_value = "hola".to_string();

        //v.push(a_ref);
        Vec::push(&mut v, &mut a_value);
    }

    // nomicom is saing that if &mut self Type was variant here we have had
    // a vector containing a reference pointing to freed memory

    // but this is not the case and the compiler throws an error
}

请注意 Vec::push(&mut v, &mut a_value) 与 nomicom 示例中的 overwrite(&mut forever_str, &mut &*string) 的相似之处。


这是我最初的印象,修改 Box<T>Vec<T> 需要获取一个 &mut self 引用,这就是所提到的可变引用。但这与“你只能通过可变引用将值存储在它们中!”的说法不符,因为您可以使用初始值实例化 Box 而不涉及可变引用。 - L.Y. Sim
嗨,Sim,我添加了一段代码片段。希望它能有所帮助。 - attdona
因此,虽然Vec::push()可以愉快地接受&mut Vec<&'a str>&mut self参数,但它不能(也不应该)接受&mut Vec<&'b str>,因为它不是前者的子类型。这就防止我们能够将无法存活足够长时间的项目存储到Vec<&'b str>中(例如“hola”)。这就是当nomicon说采用可变引用会使“整个类型不变”时的含义。 - L.Y. Sim
是的,你准确地总结了我对于与生命周期相关的方差的理解。 - attdona
说实话,你所说的似乎是最有道理的。将push()展示为一个合格的方法调用真的帮助了我。我选择你的答案作为最佳答案。我们将看看nomicon维护者在这个公开问题中会说些什么。 - L.Y. Sim
显示剩余3条评论

1

自从在Nomicon存储库中提出问题以来,维护者已经引入对该部分的修订, 我认为这个修订更加清晰明了。修订已合并。我认为我的问题已经得到了回答。

下面我简要总结一下我的了解。

与我的问题相关的部分现在读起来如下所示(重点是我的):

Box and Vec are interesting cases because they're covariant, but you can definitely store values in them! This is where Rust's typesystem allows it to be a bit more clever than others. To understand why it's sound for owning containers to be covariant over their contents, we must consider the two ways in which a mutation may occur: by-value or by-reference.

If mutation is by-value, then the old location that remembers extra details is moved out of, meaning it can't use the value anymore. So we simply don't need to worry about anyone remembering dangerous details. Put another way, applying subtyping when passing by-value destroys details forever. For example, this compiles and is fine:

 fn get_box<'a>(str: &'a str) -> Box<&'a str> {
     // String literals are `&'static str`s, but it's fine for us to
     // "forget" this and let the caller think the string won't live that long.
     Box::new("hello") }

If mutation is by-reference, then our container is passed as &mut Vec<T>. But &mut is invariant over its value, so &mut Vec<T> is actually invariant over T. So the fact that Vec<T> is covariant over T doesn't matter at all when mutating by-reference.

这里的关键点在于 &mut Vec<T> 对于 T 的不变性与 &mut T 对于 T 的不变性之间的并行。如前所述,修订后的 nomicon 部分解释了为什么一般的 &mut T 不能协变于 T&mut T 借用了 T,但它并不拥有 T,这意味着还有其他东西引用了 T 并对其生命周期有一定的期望。但是,如果我们允许将 &mut T 协变于 T,那么 nomicon 示例中的 overwrite 函数就展示了我们如何从一个不同的位置(即在 overwrite 的函数体内)打破调用者位置中 T 的生命周期。
在某种意义上,允许类型构造函数对T进行协变允许我们在传递类型构造函数时“忘记T的原始生命周期”,并且这种“忘记T的原始生命周期”对于&T是可以的,因为我们没有机会通过它来修改T,但是当我们有一个&mut T时,这就很危险了,因为我们有能力在“忘记关于它的生命周期细节之后”修改T。这就是为什么&mut T需要在T上不变的原因。
似乎nomicon试图表达的观点是:Box<T>对于T的协变是可以的,因为它不会引入不安全性。
这种协变的后果之一是,当按值传递Box<T>时,我们可以“忘记T的原始生命周期”。但这并不会引入不安全性,因为当我们按值传递时,我们保证在Box<T>被移动的位置上没有T的其他用户。旧位置中没有其他人指望T的先前寿命在移动后仍然存在。

更重要的是,Box<T>T上的协变性不会在获取Box<T>的可变引用时引入不安全性,因为&mut Box<T>Box<T>上是不变的,因此在T上也是不变的。因此,类似于上面关于&mut T的讨论,我们无法通过忘记有关T的寿命细节并在之后修改它来通过&mut Box<T>执行生命周期操作。


0

我想重点是,虽然你可以将一个Box<&'static str>转换为Box<&'a str>(因为Box<T>是协变的),但你不能将&mut Box<&'static str>转换为&mut Box<&'a str>(因为&mut T是不变的)。


你说的有点道理,但我很难看出不能在需要 &mut Box<&'a str> 的地方使用 &mut Box<&'static str> 和这句话之间的联系:“它们可以是变体是可以接受的,因为你只能通过可变引用将值存储在其中!” - L.Y. Sim
在阅读了@peter-hall的回答后,我更倾向于同意他所说的'mutable reference'是指对存储在BoxVec中的项目的可变引用。但这就留下了一个问题,即在获取可变引用时,到底是什么机制使得BoxVec不变。 - L.Y. Sim
提醒一下,Nomicon已经修订了这个部分,你可能需要看一下。我在另一个回答中发布了链接并总结了我所知道的内容。 - L.Y. Sim

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