返回一个捕获引用的高阶闭包

4

我正在尝试编写一个在结构体上返回闭包的方法。这个闭包应该以任意生命周期'inner&[u8]作为参数,并返回相同类型的&'inner [u8]。为了执行其功能,闭包还需要访问结构体成员&self的(共享)引用。以下是我的代码:

#![warn(clippy::pedantic)]

// placeholder for a large type that I can't afford to clone
struct Opaque(usize);

struct MyStruct<'a>(Vec<&'a Opaque>);

impl<'a> MyStruct<'a> {
    pub fn get_func_for_index(
        &'a self,
        n: usize,
    ) -> Option<impl for<'inner> Fn(&'inner [u8]) -> &'inner [u8] + 'a> {
        // the reference to the struct member, captured by the closure
        let opaque: &'a Opaque = *self.0.get(n)?;

        Some(move |i: &[u8]| {
            // placeholder: do something with the input using the &Opaque
            &i[opaque.0..]
        })
    }
}

fn main() {
    let o1 = Opaque(1);
    let o5 = Opaque(5);
    let o7 = Opaque(7);

    let x = MyStruct(vec![&o1, &o5, &o7]);

    let drop_five = x.get_func_for_index(1 /*Opaque(5)*/).unwrap();

    let data: Vec<u8> = Vec::from(&b"testing"[..]);

    assert_eq!(drop_five(&data[..]), b"ng");
}

如果我尝试使用rustc 1.54.0 (a178d0322 2021-07-26)编译它,我会得到以下错误:

error[E0495]: cannot infer an appropriate lifetime for lifetime parameter in function call due to conflicting requirements
  --> /tmp/lifetimes.rs:16:14
   |
16 |             &i[opaque.0..]
   |              ^^^^^^^^^^^^^
   |
note: first, the lifetime cannot outlive the anonymous lifetime #1 defined on the body at 14:14...
  --> /tmp/lifetimes.rs:14:14
   |
14 |           Some(move |i: &[u8]| {
   |  ______________^
15 | |             // placeholder: do something with the input using the &Opaque
16 | |             &i[opaque.0..]
17 | |         })
   | |_________^
note: ...so that reference does not outlive borrowed content
  --> /tmp/lifetimes.rs:16:14
   |
16 |             &i[opaque.0..]
   |              ^
note: but, the lifetime must be valid for the lifetime `'a` as defined on the impl at 6:6...
  --> /tmp/lifetimes.rs:6:6
   |
6  | impl<'a> MyStruct<'a> {
   |      ^^
note: ...so that return value is valid for the call
  --> /tmp/lifetimes.rs:10:17
   |
10 |     ) -> Option<impl for<'inner> Fn(&'inner [u8]) -> &'inner [u8] + 'a> {
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: higher-ranked subtype error
  --> /tmp/lifetimes.rs:7:5
   |
7  | /     pub fn get_func_for_index(
8  | |         &'a self,
9  | |         n: usize,
10 | |     ) -> Option<impl for<'inner> Fn(&'inner [u8]) -> &'inner [u8] + 'a> {
   | |_______________________________________________________________________^

error: aborting due to 2 previous errors

For more information about this error, try `rustc --explain E0495`.

这个术语有些啰嗦,我不太理解它想要告诉我的内容。第一部分(first, the lifetime...)对我来说是有意义的,返回的切片必须不超过闭包参数的生命周期。然而,第二部分(but, the lifetime...)对我来说有些奇怪 - 方法签名中的+ 'a注释是指闭包本身(因为它捕获了&'a self.foo),而不是闭包返回的值。

在rust中是否有可能更改代码以正确地模拟此功能,或者目前不支持这种构造?


我自己无法编译它,而且我必须指出你使用的方法相当奇怪,也许这是一个XY问题 - Alexey Larionov
我知道它看起来相当牵强,因为我尽可能地将其最小化了。我想要返回的实际闭包应该作为nom解析器,即Fn(&str) -> Result<(&str, &str), nom::Err<E>> where E: ParseError<&str> + ContextError<&str> - 问题和错误仍然存在,只是更难理解。 - Lamdba
解析器显然应该能够处理任何生命周期的&str,被解析的数据和从结构体借用的数据永远不会“混合”,它们的生命周期是完全独立的。 - Lamdba
有趣!你有任何想法为什么将其转换为特质对象会有所不同吗? - Lamdba
不好意思,我完全不知道。 - Svetlin Zarev
1个回答

9
问题在这里:
Some(move |i: &[u8]| {

每个 & 都有一个明确或隐含的生命周期。&[u8] 的生命周期是什么?显然应该是“由闭包调用者选择的生命周期”(即更高阶的生命周期)。但是当编译器遇到带有自由生命周期参数的引用类型,即使在闭包的参数列表中,它也不会假定生命周期是更高阶的。关于“匿名生命周期#1”的错误消息是编译器混淆地尝试使非更高阶的生命周期工作。
理论上,编译器可以从返回类型中的 impl Fn “向后”工作,并识别闭包类型需要具有此更高阶生命周期。它在这种情况下还不够聪明,但有一种方法可以说服它:使用具有有界类型参数的本地函数来约束闭包类型,以完全符合您想要的限制。
pub fn get_func_for_index(
    &self, // note 1
    n: usize,
) -> Option<impl 'a + for<'inner> Fn(&'inner [u8]) -> &'inner [u8]> { // note 2
    // the reference to the struct member, captured by the closure
    let opaque: &'a Opaque = *self.0.get(n)?;

    // helper function to constrain the closure type
    fn check<F: Fn(&[u8]) -> &[u8]>(f: F) -> F { // note 3
        f
    }

    Some(check(move |i| {
        // placeholder: do something with the input using the &Opaque
        &i[opaque.0..]
    }))
}

游乐场

请注意以下事项:

  1. &'a self对于该函数来说过于保守,因为'a是引用self所包含的生命周期参数,而不是self被借用的生命周期。通常情况下,您几乎不应该编写&'a self&'a mut self,其中'a是来自外部作用域的命名生命周期。
  2. 我发现在一个长的trait末尾容易错过+ 'a,特别是带有返回类型的Fn trait。在这种情况下,我建议将生命周期放在前面,以明确它更与impl相关,而不是与&'inner [u8]相关。这是一种风格上的选择。
  3. Fn(&[u8]) -> &[u8]实际上与for<'inner> Fn(&'inner [u8]) -> &'inner [u8]完全相同,因为Fn trait的省略规则与普通函数的省略规则相同。两种方式都可以;我认为较短的版本更容易阅读。

类似问题


1
哦,这个技巧真棒!你说得对,我添加了太多的生命周期参数,以确保没有奇怪的省略发生 - 现在它可以工作了,我又把它们缩减回去了。谢谢! - Lamdba

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