我需要使用`let`绑定来创建一个生命周期更长的值吗?

4

我最近开始学习Rust,在编写一个测试程序时,我写了这个方法:

pub fn add_transition(&mut self, start_state: u32, end_state: u32) -> Result<bool, std::io::Error> {
    let mut m: Vec<Page>;
    let pages: &mut Vec<Page> = match self.page_cache.get_mut(&start_state) {
        Some(p) => p,
        None    => {
            m = self.index.get_pages(start_state, &self.file)?;
            &mut m
        }
    };

    // omitted code that mutates pages 
    // ...

    Ok(true)
}

代码可以正常工作,但我对m变量并不满意。如果我移除它,代码会更加简洁:

pub fn add_transition(&mut self, start_state: u32, end_state: u32) -> Result<bool, std::io::Error> {
    let pages: &mut Vec<Page> = match self.page_cache.get_mut(&start_state) {
        Some(p) => p,
        None    => &mut self.index.get_pages(start_state, &self.file)?
    };

    // omitted code that mutates pages
    // ...

    Ok(true)
}

但是我得到的是:
error[E0716]: temporary value dropped while borrowed
  --> src\module1\mod.rs:28:29
   |
26 |           let pages: &mut Vec<Page> = match self.page_cache.get_mut(&start_state) {
   |  _____________________________________-
27 | |             Some(p) => p,
28 | |             None    => &mut self.index.get_pages(start_state, &self.file)?
   | |                             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-
   | |                             |                                            |
   | |                             |                                            temporary value is freed at the end of this statement
   | |                             creates a temporary which is freed while still in use
29 | |         };
   | |_________- borrow later used here
   |
   = note: consider using a `let` binding to create a longer lived value

我完全理解这个错误,它引导我到工作的片段,但我想知道是否有更优雅和/或成语化的编写此代码的方式。我在函数开始时声明了m,只是为了防止临时变量过早释放。有没有办法告诉编译器self.index.get_pages的返回值的生命周期应该是整个add_transition函数?
进一步细节:
- Page是一个相对较大的结构体,因此我不想实现Copy特性,也不想克隆它。 - page_cache的类型是HashMap<u32, Vec<Page>> - self.index.get_pages相对较慢,我正在使用page_cache来缓存结果 - self.index.get_pages的返回类型是Result<Vec<Page>, std::io::Error>

1
对我来说看起来不错。声明变量和作用域是定义生命周期的常规方式,当你需要这样做时。 - trent
2个回答

4
这很正常,您的“清理”代码基本上归结为执行以下操作:
let y = {
    let x = 42;
    &x
};

在这里,很明显你不能返回对 x 的引用,因为 x 在块的末尾被丢弃了。当使用临时值时,这些规则不会改变: self.index.get_pages(start_state, &self.file)? 创建一个在块结尾处被丢弃的临时值 (第29行),因此你不能返回对它的引用。

通过 m 来绕过这个问题,现在将该临时值移动到上一级绑定的 m 中,使其能够在pages可以使用的时间内存在。

现在有另外几种方法。我猜 page_cache 是一个 HashMap 吧?那么你可以尝试像这样做:let pages = self.page_cache.entry(start_state).or_insert_with(||self.index.get_pages(...))?;。但是这种方法唯一的问题在于 get_pages 返回一个 Result,而当前缓存存储的是 Vec<Page>(只有 Ok 分支)。你可以适应缓存以实际存储 Result,这在语义上也更好,因为你要缓存该函数调用的结果,那么为什么不对 Err 也这样做呢?但是如果你有一个不缓存 Err 的好理由,那么你目前的方法也可以正常工作。


我明白,我想知道是否有一种方法可以告诉编译器,'self.index.get_pages' 的返回值的生命周期必须是整个 'add_transition' 函数,而不需要声明一个从未使用过的变量。 - didymus
@didymus 据我所知,你无法强制一个临时变量的生命周期超过其作用域,除非将所有权移交给更长寿的上下文(绑定),就像你所做的那样。即使你可以通过显式生命周期来进行一些黑客操作,这也不是最好或者最符合惯用法的方法,编译器给出绑定提示是有原因的。 - KillianDS

0

你的方法可能是最有效的方式,但从理论上讲并不是必需的,还有一种更优雅的方法。

另一种做法是在这种情况下使用特质对象 —— 将变量的类型设为 dyn DerefMut<Vec<Page>>。这基本上意味着该变量可以持有任何实现了特质 DerefMut<Vec<Page>>> 的类型,其中两种实现了该特质的类型是 &mut Vec<Page>Vec<Page>,在这种情况下,变量可以持有其中任意一种类型,但内容只能通过 DerefMut 引用。

因此,以下代码可作为说明:

struct Foo {
    inner : Option<Vec<i32>>,
}

impl Foo {
    fn new () -> Self {
        Foo { inner : None }
    }
    fn init (&mut self) {
        self.inner = Some(Vec::new())
    }
    fn get_mut_ref (&mut self) -> Option<&mut Vec<i32>> {
        self.inner.as_mut()
    }
}

fn main () {
    let mut foo : Foo = Foo::new();
    let mut m   : Box<dyn AsMut<Vec<i32>>> = match foo.get_mut_ref() {
        Some(r) => Box::new(r),
        None    => Box::new(vec![1,2,3]),
    };
        m.as_mut().as_mut().push(4);
}

关键在于类型Box<dyn AsMut<Vec<i32>>>;这意味着它可以是一个持有任何类型的盒子,只要该类型实现了AsMut<Vec<i32>>接口。因为它被封装在盒子中,我们还需要使用.as_mut().as_mut()来获取实际的&mut <Vec<i32>>

由于不同类型可能具有不同的大小,它们也不能在堆栈上分配,因此必须在某个指针后面,通常选择Box,在这种情况下,一个不拥有其指向物的普通指针将面临与您面对的类似问题。

有人可能会认为这段代码更优雅,但你的代码肯定更高效,而且不需要进一步的堆分配。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接