我已经用HashMap<u32, u32>
成功地实现了Cacher,我想按照书中的建议用完全通用的类型来实现它。目前我的代码看起来是这样的,但它没有编译,我没有想法。编译错误与在value()
方法中移动v
有关,如果我将整个代码更改为接受&u32
而不是u32
,则问题仍然存在。
use std::{thread, time::Duration, collections::HashMap, hash::Hash};
struct Cacher<T, K, V>
where
T: Fn(K) -> V,
K: Eq + Hash
{
calculation: T,
values: HashMap<K, V>,
}
impl<T, K, V> Cacher<T, K, V>
where
T: Fn(K) -> V,
K: Eq + Hash
{
fn new(calculation: T) -> Self {
Cacher {
calculation,
values: HashMap::new(),
}
}
fn value(&mut self, arg: K) -> V {
if let Some(v) = self.values.get(&arg) {
*v
} else {
let v = (self.calculation)(arg);
self.values.insert(arg, v);
v
}
}
}
// rest of the code
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(simulated_user_specified_value, simulated_random_number);
}
fn generate_workout(intensity: u32, random_number: u32) {
let mut expensive_closure: Cacher<_, u32, u32> = Cacher::new(|num: u32| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
num
});
if intensity < 25 {
println!("Today do {} pushups!", expensive_closure.value(intensity));
println!("Next, do {} situps!", expensive_closure.value(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!("Today run for {} minutes!", expensive_closure.value(intensity));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn call_with_different_values() {
let mut c = Cacher::new(|a| a);
let v1 = c.value(1);
let v2 = c.value(2);
assert_eq!(v1, 1);
assert_eq!(v2, 2);
}
}
编辑:修复错误的代码的最终版本:
use std::{thread, time::Duration, collections::HashMap, hash::Hash};
struct Cacher<T, K, V>
where
T: Fn(&K) -> V,
K: Eq + Hash
{
calculation: T,
values: HashMap<K, V>,
}
impl<T, K, V> Cacher<T, K, V>
where
T: Fn(&K) -> V,
K: Eq + Hash
{
fn new(calculation: T) -> Self {
Cacher {
calculation,
values: HashMap::new(),
}
}
fn value(&mut self, arg: K) -> &V {
self.values.entry(arg).or_insert_with_key(&self.calculation)
}
}
// rest of the code
fn main() {
let simulated_user_specified_value = 10;
let simulated_random_number = 7;
generate_workout(simulated_user_specified_value, simulated_random_number);
}
fn generate_workout(intensity: u32, random_number: u32) {
let mut expensive_closure: Cacher<_, u32, u32> = Cacher::new(|num: &u32| {
println!("calculating slowly...");
thread::sleep(Duration::from_secs(2));
*num
});
if intensity < 25 {
println!("Today do {} pushups!", expensive_closure.value(intensity));
println!("Next, do {} situps!", expensive_closure.value(intensity));
} else {
if random_number == 3 {
println!("Take a break today! Remember to stay hydrated!");
} else {
println!("Today run for {} minutes!", expensive_closure.value(intensity));
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn call_with_different_values() {
let mut c = Cacher::new(|a| *a);
let v1 = c.value(1);
assert_eq!(*v1, 1);
let v2 = c.value(2);
assert_eq!(*v2, 2);
}
}
在Rust中,默认情况下,对象是不可复制的。
假设我们想要缓存类型为Vec<i32>
的值。在Cacher::value()
中,我们取arg: Vec<i32>
。现在假设密钥已经在缓存映射中。我们可以获得对它的引用,但如果我们想返回类型V
,就像现在一样,即Vec<i32>
,我们必须获得引用的自有版本。现在假设如果执行*v
的代码进行编译会发生什么。当调用方销毁(丢弃)该值时,Vec
的存储将被释放,并且我们将在缓存中有一个释放的Vec
!下次我们将查找相同的密钥时,我们将获得一个无效的Vec
,我们尝试执行的任何操作都将是"未定义的行为"!这太可怕了,正是Rust设计用来防止的那种错误。
类似的事情也发生在钥匙上。假设我们的自变量是Vec<i32>
类型。当我们将它传递给(self.calculation)(arg)
的计算器时,它将释放它,当我们在下一行尝试将它存储在缓存(self.values.insert(arg, v)
)中时,我们将存储一个无效的Vec
!
有两种方法可以解决这个问题,正确的方法取决于你问题的具体细节。
第一种方法是不要让他们(消费者和计算器)破坏价值。换言之,我们不会让他们更改值,只会让检查(从技术上讲,我们可以让他们更改,但不能让销毁,但这会导致不同的问题)。或者,用Rust术语来说,我们会给它们一个引用,而不是拥有的值。这将有一个明显的缺点,即他们无法改变它:在某些情况下,他们可能被要求改变,然后第二种选择可能更可取。
试图在代码中对此进行建模会给我们带来一些错误:
T: Fn(K) -> V,
// Replace with
T: Fn(&K) -> V,
// And
fn value(&mut self, arg: K) -> &V {
if let Some(v) = self.values.get(&arg) {
v
} else {
let v = (self.calculation)(&arg);
self.values.insert(arg, v);
&v
}
}
游乐场。
error[E0502]: cannot borrow `self.values` as mutable because it is also borrowed as immutable
--> src/lib.rs:29:13
|
24 | fn value(&mut self, arg: K) -> &V {
| - let's call the lifetime of this reference `'1`
25 | if let Some(v) = self.values.get(&arg) {
| --------------------- immutable borrow occurs here
26 | v
| - returning this value requires that `self.values` is borrowed for `'1`
...
29 | self.values.insert(arg, v);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
error[E0515]: cannot return reference to local variable `v`
--> src/lib.rs:30:13
|
30 | &v
| ^^ returns a reference to data owned by the current function
error[E0382]: borrow of moved value: `v`
--> src/lib.rs:30:13
|
28 | let v = (self.calculation)(&arg);
| - move occurs because `v` has type `V`, which does not implement the `Copy` trait
29 | self.values.insert(arg, v);
| - value moved here
30 | &v
| ^^ value borrowed here after move
这有点让人不知所措!
第一个错误实际上是Rust借用检查器的当前限制:代码没有编译器认为的问题,但编译器无法证明这一点。未来的项目将帮助编译器实现这一点,但与此同时,我们可以做一些事情:Rust的hashmap有一个专门为insert-if-not-already-there这种情况设计的构造:条目API。使用它,我们的代码看起来像(操场):
fn value(&mut self, arg: K) -> &V {
// `&self.calculation` is really equal here to `|arg| (self.calculation)(arg)`, just shorter.
self.values.entry(arg).or_insert_with_key(&self.calculation)
}
神奇的是,我们还解决了另外两个问题!这很好,但为了了解情况,我还是会解释问题所在。
我们将v
移到hashmap中,然后使用它。这就是问题所在。再次考虑v
具有类型Vec<i32>
的情况。如果我们能使用它,我们需要在之后销毁它——但它在地图内,我们不能销毁它!出于这个原因,编译器拒绝了我们的代码,并出现了第三个错误。
至于第二个错误,我们返回对v
的引用。但是v
是一个局部变量,并且在函数结束时被销毁——所以我们将返回对无效数据的引用!
修复它们不是小事(甚至是不可能的),所以我不想在这里解释。
第二种解决方法是首先思考";为什么它与i32
、非通用版本一起工作;?
答案很简单:它之所以有效,是因为";销毁";CCD_ 25。更正式地说,i32
并没有持有某些资源,就像Vec
持有分配一样:它只是普通数据,复制它——只是一个逐位的简单复制——总是可以的。或者,用Rust术语来说,i32
实现Copy
。
这就提出了一个问题:我们能在通用版本中表达这个规则吗?即说";我们希望只允许始终有效的类型进行复制,即实现Copy
"事实证明,答案是肯定的!我们只需要添加一个where Type: Copy
(操场):
where
T: Fn(K) -> V,
K: Eq + Hash + Copy,
V: Copy,
有一种方法可以概括这一点:目前,这只允许逐位复制,也就是说,只允许完全相同的位模式可以自由复制的类型。但我们也可以允许自定义复制代码,并允许像Vec
这样的东西,在保存资源的同时,可以通过分配新内存和复制内容来复制。该特性被称为Clone
,但它要求我们在复制值时更加明确(注意,每个Copy
也是Clone
,因此它也适用于i32
等类型):
where
T: Fn(K) -> V,
K: Eq + Hash + Clone,
V: Clone,
fn value(&mut self, arg: K) -> V {
if let Some(v) = self.values.get(&arg) {
v.clone()
} else {
let v = (self.calculation)(arg.clone());
self.values.insert(arg, v.clone());
v
}
}
游乐场。
如果您不确定该选择什么选项,或者不知道代码的客户端需要什么,请选择第一个选项。它更通用,因为它允许任何类型(而不仅仅是实现Clone
的类型),如果他们需要它,他们总是可以.clone()
值,然后对它进行变异,即使这有点不方便。