我应该按值传递还是按引用传递函数对象?

8

似乎有两种不进行动态分派的将函数作为参数传递的方法:

  1. &impl Fn(TIn) -> TOut // 通过引用
  2. impl Fn(TIn) -> TOut // 通过值

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

但是,我注意到例如 Option::map 就会通过值传递其闭包,这让我想可能我做错了什么。

我应该通过值还是通过引用传递函数对象?如果没有明显的答案,那我应该考虑哪些因素?

3个回答

7

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

Fn

如 @Bubletan 在他们的回答中提到的 (链接),关键点是如果 F 实现了 Fn,则对于 &FFn 将会 自动实现

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

结果是:
  • foo(f: impl Fn() -> ())可以通过foo(callable)foo(&callable)进行调用。
  • foo(f: &impl Fn() -> ())强制调用者使用foo(&callable)并禁止foo(callable)

一般来说,当被调用者存在缺陷时,最好将选择权留给调用者,因此应优先选择第一种形式。

FnMut

对于实现了FnMutF,同样适用于&mut F,也会自动实现FnMut

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

因此,参数中也应该按值传递,让调用者选择使用foo(callable)还是foo(&mut callable)

FnOnce

有一种与FnOnce保持一致性的论点,即只能按值传递,这再次指向了按值获取Fn*家族的参数的方向。

2
最初的回答:
Fn 特性文档中提到,如果某个类型 F 实现了 Fn 特性,则 &F 也实现了 Fn 特性。
Copy 特性文档中提到,对于函数指针和闭包(当然取决于它们捕获的内容),该特性会自动实现。也就是说,当它们作为参数传递给函数时,它们会被复制。
因此,你应该选择第二个选项。
示例:
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
@Clinton 我的意思是,如果你有一个引用,即使没有解除引用,你仍然可以传递它。另一种情况下,如果你需要一个引用,在出现非引用情况时,你必须创建一个引用。在答案中给出的示例中,你将不得不传递&f - Bubletan
我仍然不明白你为什么建议采用按值传递的方法。你似乎只是说大多数情况下Fn是可复制的,但在我看来,通过引用传递始终有效。如果你指出选项1的缺点,我可能会理解为什么选项2是更好的方法。 - Clinton
@Clinton 缺点是强制间接引用。就像我说的那样,你必须写&f而不是只写f。如果你有实现了Fn的&F,你仍然可以像原来一样直接传递它。 - Bubletan

2

Option::map采用值方式接受闭包的原因是它具有以下签名:

原始答案翻译成:"最初的回答"
pub fn map<U, F: FnOnce(T) -> U>(self, f: F) -> Option<U>

因此,这意味着它需要通过值来获取它,因为 FnOnce 的定义如下:链接。最初的回答
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,否则它将是无用的

FnOnce 消耗 self 的事实意味着,如果您需要这样做,您的函数只能使用一次,即使它是一个 Fn,对吧?这似乎也很限制。 - harmic
@harmic 相反的是,“由于 Fn 和 FnMut 都是 FnOnce 的子 trait,因此任何 Fn 或 FnMut 的实例都可以在期望 FnOnce 的地方使用”。 - Stargateur

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