【问题标题】:Borrow checker issue trying to pass a function pointer as paramerter借用检查器问题试图将函数指针作为参数传递
【发布时间】:2022-01-05 23:18:21
【问题描述】:

我正在寻求帮助以了解为什么借用检查器在以下最小的非工作示例中失败,并且我很高兴了解如何正确实施我正在尝试做的事情:

use std::collections::HashSet;

struct Foo {
    data: HashSet<usize>
}

impl Foo {
    fn test<'a, F, T>(&mut self, _operation: F) -> ()
    where F: Fn(&'a HashSet<usize>, &'a HashSet<usize>) -> T,
          T: Iterator<Item=&'a usize>
    {
        let update: HashSet<usize> = vec![4, 2, 9].into_iter().collect();
        self.data = _operation(&self.data, &update).copied().collect();
    }

    fn new() -> Self {
        Foo { data: HashSet::new() }
    }
}


fn main() {
    let mut foo: Foo = Foo::new();
    foo.test(HashSet::intersection);
}

我的主要困惑是,如果我用HashSet::intersection 替换对_operation 的调用,代码就会编译。我认为参数_operation 的类型将允许我在这里将HashSet::intersectionHashSet::union 作为操作传递。

为了记录,这是我收到的错误:

error[E0495]: cannot infer an appropriate lifetime for borrow expression due to conflicting requirements
  --> src\main.rs:13:32
   |
13 |         self.data = _operation(&self.data, &update).copied().collect();
   |                                ^^^^^^^^^^
   |
note: first, the lifetime cannot outlive the anonymous lifetime defined on the method body at 8:23...
  --> src\main.rs:8:23
   |
8  |     fn test<'a, F, T>(&mut self, _operation: F) -> ()
   |                       ^^^^^^^^^
note: ...so that reference does not outlive borrowed content
  --> src\main.rs:13:32
   |
13 |         self.data = _operation(&self.data, &update).copied().collect();
   |                                ^^^^^^^^^^
note: but, the lifetime must be valid for the lifetime `'a` as defined on the method body at 8:13...
  --> src\main.rs:8:13
   |
8  |     fn test<'a, F, T>(&mut self, _operation: F) -> ()
   |             ^^
note: ...so that reference does not outlive borrowed content
  --> src\main.rs:13:32
   |
13 |         self.data = _operation(&self.data, &update).copied().collect();
   |                                ^^^^^^^^^^

For more information about this error, try `rustc --explain E0495`.
error: could not compile `aoc06` due to previous error

【问题讨论】:

    标签: rust borrow-checker


    【解决方案1】:

    您传递的参数与您声明绑定的Fn 的生命周期不匹配。

    fn test<'a, F, T>(&mut self, _operation: F) -> ()
    

    'a可能由调用者指定的任意生命周期,

        F: Fn(&'a HashSet<usize>, &'a HashSet<usize>) -> T,
    

    对于给_operation的引用必须足够,

            let update: HashSet<usize> = vec![4, 2, 9].into_iter().collect();
            self.data = _operation(&self.data, &update).copied().collect();
    

    但是这里你传入一个来自self 的借用(其生命周期未指定为比'a 更长寿)和一个来自update 的借用(这是一个局部变量,不能活得更久'a)。


    为了正确地编写这个,您需要指定_operation 可以用any 生命周期调用(因此包括局部变量借用的生命周期)。这本身就很简单:

        fn test<F, T>(&mut self, _operation: F) -> ()
        where
            F: for<'a> Fn(&'a HashSet<usize>, &'a HashSet<usize>) -> T,
    

    请注意,'a 不再是 test 的生命周期参数。相反,它是F 界限的一部分:您可以将for&lt;'a&gt; 表示法解读为“对于任何 生命周期, 我们将其称为'aF可以作为函数调用,引用 &amp;'a ...”。

    但是,这实际上不是一个解决方案,因为您还有T: Iterator&lt;Item = &amp;'a usize&gt;,它再次使用'a。目前无法编写表达这种关系的where 子句,特别是即使没有作为引用的项目,迭代器也会借用&amp;'a HashSets。

    这是当前 Rust 的一个不幸限制——它也出现在尝试编写一个函数,该函数接受一个借用输入的异步函数(在结构上与您的情况相同,Future 代替 @987654347 @)。但是,有一个解决方法:您可以为函数定义一个特征,它有一个将所有内容链接在一起的生命周期参数。 (这不会对函数的调用者施加任何额外的工作,因为该特征是为所有合适的函数实现的。)

    这是您添加了这样一个特征的代码以及对fn test() 的必要修改:

    use std::collections::HashSet;
    
    trait IteratorCallback<'a> {
        type Output: Iterator<Item = &'a usize> + 'a;
        fn call(self, a: &'a HashSet<usize>, b: &'a HashSet<usize>) -> Self::Output;
    }
    
    impl<'a, F, T> IteratorCallback<'a> for F
    where
        F: FnOnce(&'a HashSet<usize>, &'a HashSet<usize>) -> T,
        T: Iterator<Item = &'a usize> + 'a,
    {
        type Output = T;
        fn call(self, a: &'a HashSet<usize>, b: &'a HashSet<usize>) -> T {
            // Delegate to FnOnce
            self(a, b)
        }
    }
    
    struct Foo {
        data: HashSet<usize>,
    }
    
    impl Foo {
        fn test<F>(&mut self, _operation: F) -> ()
        where
            F: for<'a> IteratorCallback<'a>,
        {
            let update: HashSet<usize> = vec![4, 2, 9].into_iter().collect();
            self.data = _operation.call(&self.data, &update).copied().collect();
        }
    
        fn new() -> Self {
            Foo {
                data: HashSet::new(),
            }
        }
    }
    
    fn main() {
        let mut foo: Foo = Foo::new();
        foo.test(HashSet::intersection);
    }
    

    注意:我更改了绑定到 FnOnce 的函数,因为它比 Fn 更宽松,在这种情况下你需要的只是,但是只要你将 fn call(self, 更改为 @,同样的技术将适用于 Fn 987654354@.

    致谢:我以this Reddit comment by user Lej77 为例来研究特征技术。

    【讨论】:

    • 非常感谢您的回答!选择接受哪个答案几乎是不可能的,因为它确实是您和上述答案的结合,最终帮助我们理解了它。事实证明,我们不得不稍微修改 trait bound 以要求 F 也实现 Copy 因为我们实际上想在循环中调用它;这是针对 Advent of Code 2020 任务 #6 的。这是实际代码,我们对结果非常满意:github.com/baerenbrot/advent-of-code/blob/main/2020/aoc06/src/…
    【解决方案2】:

    问题,正如编译器消息所暗示的(尽管很神秘),是存在生命周期不匹配:_operation 期望 HashSet 引用与 'a 一样长,但 &amp;self.data 具有生命周期 @987654328 @、&amp;mut self 的省略生命周期和 &amp;update 具有不同的生命周期,持续 test 函数体的持续时间。

    要解决此问题,我们必须指定函数类型 F 接受 任意 生命周期的 HashMap 引用,而不仅仅是特定生命周期 'a - 这让编译器可以推断调用_operation 时的适当生命周期。这就是我们需要Higher-Rank Trait Bounds(HRTB)的原因:

    fn test<F, T>(&mut self, _operation: F) -> ()
        where F: for<'a> Fn(&'a HashSet<usize>, &'a HashSet<usize>) -> T,
    

    但是,这引发了另一个问题。我们如何将更高级别的生命周期'a 应用于类型参数T?不幸的是 Rust does not support higher-kinded types,但我们可以通过将函数类型 F 和更高种类的类型 T “抽象”为特征和所述特征上的关联类型来摆脱困境。

    trait Operation<'a, T: 'a> {
        type Output: Iterator<Item = &'a T>;
        fn operate(self, a: &'a HashSet<T>, b: &'a HashSet<T>) -> Self::Output;
    }
    

    Operation trait 表示对两个 HashSets 的操作,该操作返回引用的迭代器,相当于函数 HashSet::unionHashSet::intersection 等。我们可以使用以下impl 来实现这一点,它确保HashSet::intersection 等实现Operation

    impl<'a, T: 'a, I, F> Operation<'a, T> for F
    where
        I: Iterator<Item = &'a T>,
        F: FnOnce(&'a HashSet<T>, &'a HashSet<T>) -> I,
    {
        type Output = I;
    
        fn operate(self, a: &'a HashSet<T>, b: &'a HashSet<T>) -> Self::Output {
            self(a, b)
        }
    }
    

    然后我们可以在 Operation 特征上使用 HRTB,这不需要任何嵌套的更高种类的类型:

    fn test(&mut self, _operation: impl for<'a> Operation<'a, usize>) -> () {
        let update: HashSet<usize> = vec![4, 2, 9].into_iter().collect();
        self.data = _operation.operate(&self.data, &update).copied().collect();
        println!("{:?}", self.data);
    }
    

    Playground

    【讨论】:

    • 非常感谢!这里的两个答案对于更好地理解生命周期界限非常有帮助。您赢得了接受答案的硬币翻转 =)。请参阅下面的评论以获取启发此问题的实际代码的链接。干杯!
    猜你喜欢
    • 2019-05-11
    • 2020-11-08
    • 2021-12-18
    • 1970-01-01
    • 1970-01-01
    • 2013-03-16
    • 2015-01-29
    • 1970-01-01
    • 2019-08-17
    相关资源
    最近更新 更多