为何HashMap::get_mut生命周期限制比get更严苛?附结构体哈希表键问题
一、为什么HashMap::get_mut在生命周期方面比HashMap::get更严苛?
咱们先从Rust的核心借用规则说起——同一时间只能存在一个可变引用,或者多个不可变引用,二者绝对不能同时存在,这是Rust保证内存安全的关键。
先看两个方法的签名:
HashMap::get的签名:fn get(&self, key: &Q) -> Option<&V>它接收的是
&self(HashMap的共享引用),返回的&V生命周期和&self绑定。因为是共享引用,你可以同时调用多次get获取不同值的引用,甚至在持有这些引用的同时调用HashMap的其他共享方法,完全符合借用规则,所以生命周期限制比较宽松。HashMap::get_mut的签名:fn get_mut(&mut self, key: &Q) -> Option<&mut V>它接收的是
&mut self(HashMap的可变引用),返回的&mut V生命周期和&mut self深度绑定。这意味着,只要你持有这个返回的可变引用,整个HashMap就进入了独占可变借用状态:你不能再调用任何需要共享引用的方法(比如get),不能再获取另一个可变引用,甚至连再次调用get_mut都不行。这种严苛的约束是为了确保可变引用的独占性,彻底避免数据竞争和状态不一致的问题,所以它的生命周期规则自然比get严格得多。
二、带生命周期的结构体Foo<'a>作为HashMap键的问题
先看你提供的代码片段:
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!("===..."); }
首先,你这么做是完全可行的!因为你已经为Foo实现了PartialEq、Eq和Hash这三个HashMap键必须的trait——而&str的这三个trait都是基于字符串内容实现的,所以Foo的相等性和哈希值会完全由txt的内容决定,完全符合HashMap键的要求。
不过有几个生命周期细节需要注意:
- 当你把
a2插入HashMap后,a(也就是a2.txt引用的字符串)必须存活到HashMap不再使用这个键为止。因为HashMap里存储的Foo持有&a的引用,如果a提前被销毁(比如离开作用域),HashMap里的Foo就会变成悬垂引用,这是Rust绝对不允许的。 - 在你的代码里,
a、b和hm都在main函数的作用域内,它们的生命周期一致,所以不会有问题。但如果后续你想把hm返回给外部函数,或者把Foo存储到更长生命周期的容器里,就要确保被引用的字符串(比如a、b)的生命周期至少和hm一样长。
如果觉得生命周期约束太繁琐,你也可以考虑把Foo改成持有String而不是&str:
#[derive(PartialEq, Eq, Hash)] struct Foo { txt: String, }
这样Foo就不再依赖外部生命周期,使用起来更灵活,代价是会多一份字符串的内存拷贝,具体选择要看你的性能和使用场景需求。
内容的提问来源于stack exchange,提问作者Pierre-Antoine

