【问题标题】:Why is HashMap::get_mut more picky than HashMap::get regarding lifetimes?为什么 HashMap::get_mut 在生命周期方面比 HashMap::get 更挑剔?
【发布时间】:2018-10-17 16:04:23
【问题描述】:

我有一个struct Foo<'a>,它是&'a str 引用的包装。我想用Foos 作为键填充HashMap。这是一段 sn-p 代码 (open it in playground):

use std::collections::HashMap;

#[derive(PartialEq, Eq, Hash)]
struct Foo<'a> {
    txt: &'a str,
}

fn main() {
    let a = "hello".to_string();
    let a2 = Foo { txt: &a };
    let b = "hello".to_string();
    let b2 = Foo { txt: &b };

    let mut hm = HashMap::<Foo, u32>::new();

    hm.insert(a2, 42);
    println!("=== {:?}", hm.get(&b2));     // prints Some(42)
    println!("=== {:?}", hm.get_mut(&b2)); // prints Some(42)

    {
        let c = "hello".to_string();
        let c2 = Foo { txt: &c };
        println!("=== {:?}", hm.get(&c2));         // prints Some(42)
        // println!("=== {:?}", hm.get_mut(&c2));  // does not compile. Why?
        // hm.insert(c2, 101);                     // does not compile, but I understand why.
    }
}

这段代码可以完美编译和运行,但是如果我取消注释最后两行代码,编译器会报错。更准确地说,它抱怨 c2 中的借来的值不够长。

对于最后一个 (insert),这是完全可以理解的:我不能将 c2 移动到 HashMap,它的寿命比 c2c 借来的数据要长。

但是,我不明白为什么倒数第二行 (get_mut) 有同样的问题:在这种情况下,借用的数据应该只在调用 get_mut 期间是必需的,它不是移入HashMap

更令人惊讶的是,上面的get 可以完美运行(正如我所料),并且getget_mutk 参数方面具有相同的签名......


再挖一点之后, 我用普通引用(而不是嵌入引用的结构)重现了这个问题。

use std::collections::HashMap;

fn main() {
    let a = 42;
    let b = 42;

    let mut hm = HashMap::<&u32,u32>::new();

    hm.insert(&a, 13);
    println!("=== {:?}", hm.get(&&b));     // prints Some(13)
    println!("=== {:?}", hm.get_mut(&&b)); // prints Some(13)

    {
        let c = 42;
        println!("=== {:?}", hm.get(&&c));        // prints Some(13)
        //println!("=== {:?}", hm.get_mut(&&c));  // does not compile. Why?
    } 
}

(open in playground)

同样,取消注释最后一行会导致编译器抱怨(与上面的消息相同)。

但是,对于这个特定示例,我发现了一个有趣的解决方法:在最后一行中将 &amp;&amp;c 替换为 &amp;c 解决了问题——实际上,可以在对 @987654348 的所有调用中将 &amp;&amp; 替换为 &amp; @ 和 get_mut。我想这与 &amp;T 实现 Borrow&lt;T&gt; 有关。

我不明白在这个解决方法中究竟是什么说服了编译器做我想让它做的事情。而且我不能直接将它应用到我的原始代码中,因为我不使用 references 作为键,而是使用嵌入引用的对象,所以我无法将 &amp;&amp; 替换为 &amp;...

【问题讨论】:

  • 我相信Why does linking lifetimes matter only with mutable references? 已经回答了这个问题。如果您不同意,请edit您的问题解释这与现有答案有何不同。否则,我们可以将其标记为已回答。
  • 另外,编译器如何知道 get_mut 不会将参数存储在HashMap 中,只使用函数的签名?跨度>
  • 我不完全确定我的问题与您提到的问题相同。大多数情况下,不同之处在于,在我的例子中,只涉及对Foo(借用类型)的不可变引用。
  • 但是,您对get_mut 很清楚:区别(与get)不在于参数k,而在于self 是可变的。
  • 如果您的问题的答案包含“方差”一词,那么您将度过一段糟糕的时光。对于可能的回答者,我发现以下一些事实很有启发性:&amp;TT 的变体,&amp;mut TT 中是不变的,HashMap&lt;K, V&gt;K 的变体。祝你好运!

标签: rust lifetime


【解决方案1】:

问题的出现是因为不可变引用在其(被引用的)类型上是可变的,而可变引用在其类型上是不变的。

Nomicon 是理解这个概念的好书。

HashMap:getget_mut

进一步缩小,这是重现问题的更简单的代码:

#![allow(unused_variables)]
use std::collections::HashMap;

fn main() {
    let mut hm = HashMap::<&u32, u32>::new();    // --+ 'a
    let c = 42;                                    // |  --+ 'b
                                                   // |    |
    HashMap::<&u32, u32>::get(&mut hm, &&c);       // |    |
    // HashMap::<&u32, u32>::get_mut(&mut hm, &&c);// |    |
}                                                  // +    +

不可变的情况

考虑HashMap::get的签名:

fn get<Q: ?Sized>(&self, k: &Q) -> Option<&V>
    where K: Borrow<Q>, Q: Hash + Eq

在这种情况下,&amp;Q&amp;&amp;'b u32get 的接收者是 &amp;Self

不可变引用的变体性质意味着 &amp;HashMap&lt;&amp;'a u32, u32&gt; 可用于需要 &amp;HashMap&lt;'b u32, u32&gt; 的地方。

由于这条规则,编译器会考虑原始调用:

HashMap::<&'a u32, u32>::get(&hm, &&'b c);

相当于:

HashMap::<&'b u32, u32>::get(&hm, &&'b c);

编译器从接口推断,并且仅从接口,方法实现不会引入泄漏:编译成功。

可变情况

考虑HashMap::get_mut的签名:

fn get_mut<Q: ?Sized>(&mut self, k: &Q) -> Option<&mut V>
    where K: Borrow<Q>, Q: Hash + Eq

同样在这种情况下&amp;Q&amp;&amp;'b u32,但get_mut 的接收者是&amp;mut Self

可变引用的不变性意味着 &amp;mut HashMap&lt;&amp;'a u32, u32&gt; 不能用于需要 &amp;mut HashMap&lt;&amp;'b u32, u32&gt; 的地方。

由于此规则,编译器会抛出错误,因为通过分析接口:

HashMap::<&'a 32, u32>::get_mut(&mut hm, &&'b c);

编译器不能排除,例如,get_mut 可能存储一个生命周期为'b 的键。

这样的键不能超过hm HashMap:编译失败。

【讨论】:

  • 非常感谢,这澄清了很多事情(也因为我之前读过this,在@shepmaster 的建议下)。话虽如此,这里很遗憾,因为我们知道(与编译器不同)get_mut 不会泄漏 HashMap 中传递的密钥:-/
  • 现在是对的。非常感谢@trentcl 的更正!
  • 我仍然不明白为什么用&amp;c 替换&amp;&amp;c 有效,但是...我理解它为什么在类型方面有效(K=&amp;u32 实现Borrow&lt;Q&gt; 其中Q=u32) ;但我不明白为什么编译器突然对这种情况下的生命周期感到满意......
  • &amp;'a u32 implements Borrow&lt;u32&gt; for any lifetime 'a&amp;'a u32 implements Borrow&lt;&amp;'a u32&gt; by the reflexive implementation,但 &amp;'a u32 实现 Borrow&lt;&amp;'b u32&gt;,即使 'a: 'b。这是因为特征(包括Borrow)始终对其参数保持不变。
  • @Pierre-Antoine 这也意味着如果你愿意创建一个新类型的结构来放入你的HashMap,你可以自己写那个缺失的毯子impl并获得(几乎)要编译的原始示例:playground。所以你的直觉是对的:限制过于保守。
猜你喜欢
  • 2013-04-23
  • 2018-08-05
  • 1970-01-01
  • 2015-01-04
  • 1970-01-01
  • 1970-01-01
  • 2015-10-03
  • 2021-10-27
  • 2015-11-11
相关资源
最近更新 更多