在结构中存储通用闭包

时间:2019-11-01 14:24:47

标签: generics rust

我目前正在学习Rust。在这本书的chapter 13部分中,有一个Cacher struct的示例。缓存器的思想是,仅在请求并存储该值后才对其求值。在该示例中,缓存器的输入为i32,输出为i32。由于我想使它更有用,所以我希望缓存不接受任何输入并生成任何类型的值(如果您很熟悉,基本上是Lazy<T> type from .NET)。

我的第一个想法是使用通用注释修改给定的Cacher

struct Cacher<TCalc, TVal>
    where TCalc: Fn() -> TVal
{
    calculation: TCalc,
    value: Option<TVal>,
}

impl<TCalc, TVal> Cacher<TCalc, TVal>
    where TCalc: Fn() -> TVal
{
    fn new(calculation: TCalc) -> Cacher<TCalc, TVal> {
        Cacher {
            calculation,
            value: None,
        }
    }

    fn value(&mut self) -> TVal {
        match self.value { // cannot move out of `self.value.0` which is behind a mutable reference
            Some(v) => v,
            None => {
                let v = (self.calculation)();
                self.value = Some(v);
                v // use of moved value: `v`
            },
        }
    }
}

如注释所示,这在value方法中引发了一些错误。
然后,我尝试了许多事情,并为value方法提出了可行的解决方案。请注意,它现在返回&TVal而不是TVal,但这并没有真正困扰我。

fn value(&mut self) -> &TVal {
    if let None = self.value {
        let v = (self.calculation)();
        self.value = Some(v);
    }

    self.value.as_ref().unwrap()
}

我可以这样创建和使用此缓存器:

let mut expensive_val = Cacher::new(|| {
    println!("Calculating value..");
    "my result"
});

println!("Cacher was created.");
println!("The value is '{}'.", expensive_val.value());
println!("The value is still '{}'.", expensive_val.value());

// Cacher was created.
// Calculating value..
// The value is 'my result'.
// The value is still 'my result'.

这很好用,但是我觉得有两个类型参数对此是多余的,所以我尝试删除第一个(TCalc)。经过研究后,我想到了这一点:

struct Cacher<'a, T>
{
    calculation: &'a dyn Fn() -> T,
    value: Option<T>,
}

impl<'a, T> Cacher<'a, T>
{
    fn new(calculation: &'a dyn Fn() -> T) -> Cacher<T> {
        Cacher {
            calculation,
            value: None,
        }
    }

    fn value(&mut self) -> &T {
        if let None = self.value {
            let v = (self.calculation)();
            self.value = Some(v);
        }

        self.value.as_ref().unwrap()
    }
}

此缓存器仍然有效,但是现在我必须传递对闭包的引用,而不是闭包本身。

let mut expensive_val = Cacher::new(&|| { // Note the &
    println!("Calculating value..");
    "my result"
});

我没有发现任何不利之处,但是有没有参考的方法吗?我的意思是使用单个类型参数,同时仍传递闭包而不是引用。只需尝试直接存储Fn() -> T就会得到the size for values of type `(dyn std::ops::Fn() -> T + 'static)` cannot be known at compilation time

Ps。也许我说的有些话是错误的,或者是不是生锈的方式,所以如果您可以纠正这些错误,请:)

1 个答案:

答案 0 :(得分:1)

您正在挖一个洞。让我们退后一步来了解为什么会发生这种情况。

您的目标是缓存一个可能昂贵的计算,以便不必在随后的煤上重复进行计算。但是,这意味着调用返回将返回对最终结果的引用或完整值。

这个选择比实现中看起来要重要得多。

按值返回的情况

您的Cacher结构变为:

struct Cacher<TCalc, TVal: Clone>
    where TCalc: Fn() -> TVal
{
    calculation: TCalc,
    value: Option<TVal>,
}

然后您的访问者变为:

fn value(&mut self) -> TVal {
    match &self.value {
        Some(r) => r.clone(),
        None => {
            self.value = Some((self.calculation)());
            self.value.clone().unwrap()
        }
    }
}

这是简洁明了的方法,它依赖于您将值作为自身的副本进行传递;进行此操作时值得做的一件事是检查此克隆实际上是一项低成本操作。

参考方法

您的方法遇到两个问题,这是由于您相对缺乏Rust的经验(很好!我们都从某个地方开始了):

  1. 因为您返回了引用,所以不是并不意味着您要关闭的闭包本身必须是引用。实际上,这样做是有害的,因为闭包可能已经已经关闭了其他状态。将其作为参考是徒劳的
  2. 终身限制是完全没有必要的

结果,我们剩下以下内容:

struct Cacher<TCalc, TVal>
    where TCalc: Fn() -> TVal
{
    calculation: TCalc,
    value: Option<TVal>,
}
impl<TCalc, TVal> Cacher<TCalc, TVal>
    where TCalc: Fn() -> TVal {
    pub fn new(calculation: TCalc) -> Cacher<TCalc, TVal> {
        Cacher {
            calculation,
            value: None
        }
    }
    pub fn get_mut(&mut self) -> &mut TVal {
        if self.value.is_none() {
            self.value = Some((self.calculation)());
        }
        self.value.as_mut().unwrap()
    }
}

这提供了对其值的可变引用,并在不存在时预先创建它,从而满足要求。

这仍然存在问题,最臭名昭著的事实是,如果您想要对内部值的不可变引用,则仍然需要对容器进行可变的借用,而这可以通过内部突变结构来解决,但这是另一回事了。