How do I build a Cacher in Rust without relying on the Copy trait?
我正在尝试实现Rust书第13章中提到的Cacher并遇到麻烦。
我的Cacher代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| use std::collections::HashMap;
use std::hash::Hash;
pub struct Cacher<T, K, V>
where
T: Fn(K) -> V,
{
calculation: T,
values: HashMap<K, V>,
}
impl<T, K: Eq + Hash, V> Cacher<T, K, V>
where
T: Fn(K) -> V,
{
pub fn new(calculation: T) -> Cacher<T, K, V> {
Cacher {
calculation,
values: HashMap::new(),
}
}
pub fn value(&mut self, k: K) -> &V {
let result = self.values.get(&k);
match result {
Some(v) => {
return v;
}
None => {
let v = (self.calculation)(k);
self.values.insert(k, v);
&v
}
}
}
} |
和我对该库的测试用例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| mod cacher;
#[cfg(test)]
mod tests {
use cacher::Cacher;
#[test]
fn repeated_runs_same() {
let mut cacher = Cacher::new(|x| x);
let run1 = cacher.value(5);
let run2 = cacher.value(7);
assert_ne!(run1, run2);
}
} |
运行测试用例时遇到以下问题:
error[E0499]: cannot borrow cacher as mutable more than once at a time
每次我生成run1和run2值时,它都会尝试借用Cacher作为可变借借。我根本不明白为什么要借用-我认为cacher.value()应该返回对存储在Cacher中而不是借用的项目的引用。
error[E0597]: v does not live long enough指向v我在value()为None的情况下返回。如何正确地将v移到HashMap中并赋予其与HashMap相同的生存期?显然,生存期将随着它的返回而到期,但是我只想返回对其的引用以用作value()的返回。
value()中的error[E0502]: cannot borrow self.values as mutable because it is also borrowed as immutable。 self.values.get(&k)是不可变的借项,而self.values.insert(k,v)是可变的借项-尽管我认为.get()是不可变的借项,而.insert()是所有权转让。
以及其他一些与移动有关的错误,我应该能够分别处理。这些是更根本的错误,表明我误解了Rust的所有权概念,但是重新阅读本书的内容并不清楚我错过了什么。
我认为这里有很多问题需要探讨:
首先,对于函数value(&mut self, k: K) -> &V的定义;编译器将为您插入生存期,使其变为value(&'a mut self, k: K) -> &'a V。这意味着self的生存期不会因函数而缩短,因为存在具有相同生存期的引用离开函数,并且生存期与作用域一样长。由于它是可变参考,因此您不能再次借用它。因此,错误error[E0499]: cannot borrow cacher as mutable more than once at a time。
其次,调用calculation函数,该函数返回该函数value()的某些内部范围内的值,然后返回对其的引用,这是不可能的。您期望参考的寿命比参考对象的寿命长。因此,错误error[E0597]: v does not live long enough
第三个错误涉及其中。如第一条语句所述,您看到let result = self.values.get(&k);导致k保持不变,直到函数结束。返回的result将在您的函数value()中有效,这意味着您不能在同一范围内借用(可变),从而产生错误
error[E0502]: cannot borrow self.values as mutable because it is also borrowed as immutable in value() self.values.get(&k)
您的k必须是Clone,原因是k将被移到函数calculation中,从而使其在insert中不可用。
因此,将k作为Clone,Cacher实现将是:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| impl<T, K: Eq + Hash + Clone, V> Cacher<T, K, V>
where
T: Fn(K) -> V,
{
pub fn new(calculation: T) -> Cacher<T, K, V> {
Cacher {
calculation,
values: hash_map::HashMap::new(),
}
}
pub fn value(&mut self, k: K) -> &V {
if self.values.contains_key(&k) {
return &self.values[&k];
}
self.values.insert(k.clone(), (self.calculation)(k.clone()));
self.values.get(&k).unwrap()
}
} |
此处的生存期基于分支控制流。 if self.values.contains_key ...块始终返回,因此,仅当if self.values.contains_key ...为false时才能执行if块之后的代码。为if条件创建的微小作用域将仅存在于条件检查中,即为if self.values.contains_key(...获取(并返回)的引用将消失于该微小作用域。
有关更多信息,请参阅NLL RFC
如@jmb在他的回答中提到的那样,为了使测试正常进行,V必须是Clone(impl <... V:Clone> Cacher<T, K, V>)才能按值返回或使用诸如Rc的共享所有权来避免克隆费用。
例如
1 2
| fn value(&mut self, k: K) -> V { ..
fn value(&mut self, k: K) -> Rc<V> { .. |
- 使用Rc(尚未对@jmbs答案发表评论):play.rust-lang.org/
-
谢谢维克拉姆。我可以详细了解一下测试用例吗?我试图在不使用Rc的情况下实现它(为了熟悉该语言),但是又遇到了双重可变借入问题。考虑到它仅引用了V,因此不应该是一个问题。 play.rust-lang.org/
-
@WarSame,是的,value()本身的功能签名很好,但是要与您的test协同工作,事实并非如此。 (如前所述)。我知道,您已经声明了V为Clone类型,那么为什么不将签名更改为fn value(&mut self, k: K) -> V { ..?因为您不想使用Rc。 play.rust-lang.org/。
-
@WarSame,另一种方法可能是使用原始指针。这将使您保持value()的签名不变,并且V不必是Clone类型。请注意,通常将原始指针的使用视为"不安全",但是如果严格来讲这种特殊情况,则看起来不错。 play.rust-lang.org/
-
谢谢维克拉姆。我是Rust的新手,还没有玩过克隆或许多其他元素。非常感谢您耐心的解释以及您提供的其他详细信息/选项。我绝对希望避免使用原始指针,因为我知道它们可能非常危险。我切换到克隆方法,它可以工作!
返回对值的引用与借用该值相同。由于该值归缓存器所有,因此它也隐式借用了缓存器。这是有道理的:如果您引用了缓存器中的某个值,然后销毁了该缓存器,那么对您的引用会发生什么?还请注意,如果您修改了缓存器(例如,通过插入新元素),这可能会重新分配存储空间,这将使对存储在其中的值的任何引用无效。
您需要您的值至少为Clone,以便Cacher::value可以按值而不是按引用返回。如果您的值太昂贵而无法克隆,并且所有调用方都获得相同的实例是可以的,则可以使用Rc。
获取存储在HashMap中的实例(而不是分配给其构建的临时对象)的天真的方法是在将值插入映射后调用self.values.get (k).unwrap()。为了避免计算值在地图中的位置两倍的开销,可以使用Entry界面:
1 2 3
| pub fn value(&mut self, k: K) -> Rc<V> {
self.values.entry (&k).or_insert_with (|| Rc::new (self.calculation (k)))
} |
我相信我对第2点的回答也可以解决这一点。