Хранение общего замыкания в структуре - PullRequest
0 голосов
/ 01 ноября 2019

Я сейчас нахожусь в процессе изучения Rust. В главе 13 из книги в этой части приведен пример Cacher struct . Идея, лежащая в основе кэширования, заключается в том, что значение оценивается только после того, как оно запрошено, а затем сохранено. В этом примере ввод кэша равен i32, а также вывод i32. Поскольку я хотел сделать его немного более полезным, я хотел, чтобы кэшировщик не брал никаких входных данных и генерировал значение любого типа (в основном тип Lazy<T> из .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`
            },
        }
    }
}

Как вы можете заметить, комментируя комментарии, это породило некоторые ошибки на моем пути в *Метод 1019 *.
Затем я попробовал много вещей и нашел рабочее решение для метода 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"
});

Iне вижу в этом никакого недостатка, но есть ли способ сделать это без ссылки? Я имею в виду с одним параметром типа и все еще передавая закрытие вместо ссылки. Простая попытка сохранить Fn() -> T напрямую приведет к the size for values of type `(dyn std::ops::Fn() -> T + 'static)` cannot be known at compilation time.

Ps. Может быть, я сказал что-то неправильно, или не так, как вы делаете это в ржавчине , поэтому, если вы можете исправить меня, пожалуйста, сделайте:)

1 Ответ

1 голос
/ 01 ноября 2019

Ты копаешься в яме. Давайте сделаем шаг назад, чтобы понять, почему это происходит.

Ваша цель - кэшировать потенциально дорогостоящее вычисление, чтобы не повторять его для последующих углей. Это означает, однако, что при возврате вашего вызова будет возвращена либо ссылка на конечный результат, либо полное значение.

Этот выбор намного, намного важнее, чем кажетсядля вашей реализации.

Случай, когда вы возвращаете по значению

Ваша 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()
    }
}

Это обеспечиваетизменяемая ссылка на его значение и создает его заранее, если он не существует, таким образом выполняя требования.

Все еще существуют проблемы с этим, наиболее известный тот факт, что, если вы хотели неизменную ссылку на внутреннийзначение, вам по-прежнему нужен изменчивый заем для контейнера, и это то, что вы можете решить с помощью внутренней структуры мутаций, но это история для другого дня.

...