【问题标题】:How do I avoid incurring in lifetime issues when refactoring a function?重构函数时如何避免出现生命周期问题?
【发布时间】:2022-02-06 03:06:30
【问题描述】:

Playground如果你想直接跳转到代码中。

问题

我正在尝试实现一个函数filter_con<T, F>(v: Vec<T>, predicate: F),它允许通过async 谓词对Vec 进行并发过滤。

也就是说,而不是做:

let arr = vec![...];
let arr_filtered = join_all(arr.into_iter().map(|it| async move {
    if some_getter(&it).await > some_value {
        Some(it)
    } else {
        None
    }
}))
.await
.into_iter()
.filter_map(|it| it)
.collect::<Vec<T>>()

每次我需要过滤Vec 时,我都希望能够:

let arr = vec![...];
let arr_filtered = filter_con(arr, |it| async move { 
  some_getter(&it).await > some_value 
}).await

暂定实施

我已将函数提取到它自己的函数中,但我遇到了生命周期问题

async fn filter_con<T, B, F>(arr: Vec<T>, predicate: F) -> Vec<T>
where
    F: FnMut(&T) -> B,
    B: futures::Future<Output = bool>,
{
    join_all(arr.into_iter().map(|it| async move {
        if predicate(&it).await {
            Some(it)
        } else {
            None
        }
    }))
    .await
    .into_iter()
    .filter_map(|p| p)
    .collect::<Vec<_>>()
}
error[E0507]: cannot move out of a shared reference

我不知道我要从谓词中移出什么?

更多详情,请参阅playground

【问题讨论】:

  • 好的,所以我认为 FnMut 是 map() 参数。不知道我们是否可以转换它,因为我们在这里没有修改任何东西......
  • 在 Discord 上讨论后,here's as close as I can get。我相信剩余的错误是可以解决的,但我目前没有看到矩阵。

标签: rust async-await


【解决方案1】:

您将无法将谓词设为FnOnce,因为如果您的Vec 中有10 个项目,您将需要调用谓词10 次,但FnOnce 只能保证它可以调用一次,这可能会导致这样的事情:

let vec = vec![1, 2, 3];
let has_drop_impl = String::from("hello");

filter_con(vec, |&i| async {
  drop(has_drop_impl);
  i < 5
}

所以F 必须是FnMutFn。标准库Iterator::filter 采用FnMut,尽管这可能会造成混淆(闭包 的捕获变量需要可变引用,而不是迭代器的元素)。

因为谓词是FnMut,所以任何调用者都需要能够获得&amp;mut F。对于Iterator::filter,这可以用来做这样的事情:

let vec = vec![1, 2, 3];
let mut count = 0;

vec.into_iter().filter(|&x| {
  count += 1;  // this line makes the closure an `FnMut`
  x < 2
})

但是,通过将迭代器发送到 join_all,您实际上是允许异步运行时根据需要安排这些调用,可能同时进行,这将导致别名 &amp;mut T,这始终是未定义的行为。此问题是同一问题https://github.com/rust-lang/rust/issues/69446 的略微精简版本。

我仍然不是 100% 了解细节,但似乎编译器在这里很保守,甚至不允许您首先创建闭包以防止出现可靠性问题。

我建议让您的函数只接受Fns。这样,您的运行时可以随意调用该函数。这确实意味着您的闭包不能具有可变状态,但这在 tokio 应用程序中不太可能成为问题。对于计数示例,“正确”的解决方案是使用AtomicUsize(或等效项),它允许通过共享引用进行突变。如果您在 filter 调用中引用可变状态,它应该是线程安全的,并且线程安全数据结构通常允许通过共享引用进行突变。

鉴于该限制,以下给出了您期望的答案:

async fn filter_con<T, B, F>(arr: Vec<T>, predicate: F) -> Vec<T>
where
    F: Fn(&T) -> B,
    B: Future<Output = bool>,
{
    join_all(arr.into_iter().map(|it| async {
        if predicate(&it).await {
            Some(it)
        } else {
            None
        }
    }))
    .await
    .into_iter()
    .filter_map(|p| p)
    .collect::<Vec<_>>()
}

Playground

【讨论】:

  • 谢谢@cameron1024!我查了Fn/FnOnce/FnMut 的区别,你的解释是正确的!但是,在这里您使用的是向量的项目是复制的事实,这只是为了简单起见(请参阅上面的注释声明)。我编辑了playground 以更好地反映这一点。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-01-25
  • 1970-01-01
  • 2017-12-27
  • 1970-01-01
  • 2015-11-15
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多