【问题标题】:Should I pass function objects by value or by reference?我应该通过值还是通过引用传递函数对象?
【发布时间】:2019-06-03 02:46:48
【问题描述】:

似乎有两种方法可以将函数作为不进行动态调度的参数传递:

  1. &impl Fn(TIn) -> TOut // By reference
  2. impl Fn(TIn) -> TOut // By value

假设函数是纯函数(即可以多次调用),我最初的想法是最好的方法是通过引用传递。这意味着一个函数对象可以被多次使用(因为所有权不会被转移),在更常见的情况下,它只是一个匿名闭包,引用间接应该被优化掉,因为编译器确切地知道函数本身(所以它可以被内联)。

但是,例如,我注意到 Option::map 通过值传递了它的闭包,这让我觉得也许我做错了什么。

我应该通过值还是通过引用传递函数对象?如果这两种方法都没有明确的答案,我应该考虑哪些因素?

【问题讨论】:

    标签: rust


    【解决方案1】:

    TL;DR:您应该使用F: Fn() -> ()impl Fn() -> () 作为参数。

    Fn

    正如@Bubletan 提到的in their answer,关键是Fnautomatically implemented 对于&F 如果F 实现Fn

    impl<'_, A, F> Fn<A> for &'_ F
    where
        F: Fn<A> + ?Sized,
    

    结果是:

    • foo(f: impl Fn() -&gt; ()) 可以与foo(callable)foo(&amp;callable) 一起调用both
    • foo(f: &amp;impl Fn() -&gt; ()) 强制调用者使用foo(&amp;callable)并禁止foo(callable)

    一般来说,当被调用者有不利条件时,最好将选择权留给调用者,因此应该首选第一种形式。

    FnMut

    同样的逻辑适用于FnMut,如果F实现FnMut,则&amp;mut F也是automatically implemented

    impl<'_, A, F> FnMut<A> for &'_ mut F
    where
        F: FnMut<A> + ?Sized, 
    

    因此也应该在参数中按值传递,让调用者选择他们更喜欢foo(callable) 还是foo(&amp;mut callable)

    FnOnce

    有与FnOnce一致的论点,只能按值传递,这又指向了Fn*家族的论点按值取值的方向。

    【讨论】:

      【解决方案2】:

      Option::map 采用值闭包的原因是它具有following signature

      pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U>
      

      因此这意味着它需要按值获取,因为FnOnce 的定义是following

      pub trait FnOnce<Args> {
          type Output;
          extern "rust-call" fn call_once(self, args: Args) -> Self::Output;
      }
      

      另外,这个Fn 变体是限制最少的,因此是最有用的,因为FnMut: FnOnceFn: FnMut,所以FnOnce 是最少派生的。
      所以,由此我们可以推断:

      • Option::map 试图使其论点限制最少
      • FnOnce 限制最少
      • FnOnce 需要按值取self
      • 因此Option::map 采用f 的值,否则it'd be useless

      【讨论】:

      • FnOnce 消耗 self 的事实也意味着,如果您需要,您的函数只能使用一次,即使它是 Fn,对吧?这似乎也有限制
      • @harmic 相反“由于 Fn 和 FnMut 都是 FnOnce 的子特征,因此 Fn 或 FnMut 的任何实例都可以在预期 FnOnce 的地方使用。”
      【解决方案3】:

      Fn trait 的文档指出,如果某些类型 F 实现了 Fn,那么 &F 也实现了 Fn。

      Copy trait 的文档中,提到该 trait 是针对函数指针和闭包自动实现的(当然取决于它们捕获的内容)。也就是说,它们在作为参数传递给函数时被复制。

      因此你应该选择第二个选项。

      一个例子:

      fn foo(f: impl Fn(i32) -> i32) -> i32 { f(42) }
      
      fn bar() {
          let f = |x| -> 2 * x;
          foo(f);
          foo(f); // f is copied and can thus be used
      }
      

      【讨论】:

      • 我仍然不清楚为什么我会选择第二个选项而不是第一个选项?您说:“如果某些类型 F 实现了 Fn,那么 &F 也实现了 Fn。”你没有说如果 &F 实现了 Fn 那么 F 实现了 Fn,所以这个陈述往往暗示 &F 是更一般的方法。
      • @Clinton 我的意思是,如果你有一个引用,你仍然可以在不取消引用的情况下传递它。在另一种情况下,如果您需要引用,则必须创建一个以防万一您有非引用。在答案中给出的示例中,您必须改为传递 &amp;f
      • 我仍然不明白您为什么要建议按价值方法。您似乎只是在说大多数 Fns 是可复制的,但在我看来,通过引用传递一直有效。如果您指出选项 1 的缺点,我可能会理解为什么选项 2 是更好的方法。
      • @Clinton 缺点是强制间接。就像我说的,你必须写&amp;f 而不仅仅是f。如果你有实现 Fn 的 &F,你仍然可以照原样传递它。
      猜你喜欢
      • 1970-01-01
      • 2023-04-07
      • 1970-01-01
      • 2015-04-10
      • 2012-04-28
      • 2020-04-01
      • 1970-01-01
      • 2017-01-27
      • 2012-10-31
      相关资源
      最近更新 更多