你如何将Rust函数作为参数传递?

166

我能把函数当做参数传递吗?如果不能,有什么好的替代方案吗?

我尝试了一些不同的语法,但都没有找到正确的。我知道我可以这样做:

fn example() {
    let fun: fn(value: i32) -> i32;
    fun = fun_test;
    fun(5i32);
}

fn fun_test(value: i32) -> i32 {
    println!("{}", value);
    value
}

但这不是将函数作为参数传递给另一个函数:

fn fun_test(value: i32, (some_function_prototype)) -> i32 {
    println!("{}", value);
    value
}
2个回答

239

当然可以:

fn fun_test(value: i32, f: &dyn Fn(i32) -> i32) -> i32 {
    println!("{}", f(value));
    value
}

fn times2(value: i32) -> i32 {
    2 * value
}

fn main() {
    fun_test(5, &times2);
}

由于这是 Rust,你必须考虑到 闭包的所有权和生命周期

简而言之,基本上有三种类型的闭包(可调用对象):

  1. Fn:它不能修改捕获的对象。
  2. FnMut:它可以修改捕获的对象。
  3. FnOnce:最受限制的。只能被调用一次,因为当它被调用时,它会消耗自己和它的捕获物。

请参阅 闭包何时实现 Fn、FnMut 和 FnOnce? 以获取更多详细信息。

如果你使用像指向函数的简单指针这样的闭包,则捕获设置为空,且你具有 Fn 类型。

如果你想要做更高级的操作,则必须使用 lambda 函数。

在Rust中,有适当的函数指针,与C语言中的指针类似。它们的类型例如为fn(i32) -> i32Fn(i32) -> i32FnMut(i32) -> i32FnOnce(i32) -> i32实际上是特征(traits)。函数指针始终实现这三个特征,但是Rust还具有闭包,可以将其转换为指针(取决于捕获集是否为空),但它们确实实现了其中一些特征。
因此,例如,上面的示例可以扩展:
fn fun_test_impl(value: i32, f: impl Fn(i32) -> i32) -> i32 {
    println!("{}", f(value));
    value
}
fn fun_test_dyn(value: i32, f: &dyn Fn(i32) -> i32) -> i32 {
    println!("{}", f(value));
    value
}
fn fun_test_ptr(value: i32, f: fn(i32) -> i32) -> i32 {
    println!("{}", f(value));
    value
}

fn times2(value: i32) -> i32 {
    2 * value
}

fn main() {
    let y = 2;
    //static dispatch
    fun_test_impl(5, times2);
    fun_test_impl(5, |x| 2*x);
    fun_test_impl(5, |x| y*x);
    //dynamic dispatch
    fun_test_dyn(5, &times2);
    fun_test_dyn(5, &|x| 2*x);
    fun_test_dyn(5, &|x| y*x);
    //C-like pointer to function
    fun_test_ptr(5, times2);
    fun_test_ptr(5, |x| 2*x); //ok: empty capture set
    fun_test_ptr(5, |x| y*x); //error: expected fn pointer, found closure
}

1
使用 <F:Fn...> 或不使用(..,f:&Fn...)有差别,这两者都可以工作,但我需要知道一些细节。 - Angel Angel
@AngelAngel:嗯,Fn* 是特质(trait),因此通常的 <T: Trait> vs (t: &T) 适用。非泛型解决方案的主要限制是必须使用引用。因此,如果您想要作为副本传递的 FnOnce,则必须使用泛型风格。 - rodrigo
8
请注意,使用泛型而不是特质对象更符合惯用语(即 <F: Fn..> 而不是 (f: &Fn...))。这是有原因的 - 泛型将导致静态分派,而特质对象需要动态分派。 - Vladimir Matveev
3
有趣的是,从一个接口(调用者)的角度来看,FnOnce实际上是最通用的trait-它接受所有闭包,无论它们是否读取、修改或获取捕获的状态。FnMut更为严格,它不接受获取已捕获对象所有权的闭包(但仍允许修改状态)。Fn是最为严格的,因为它不接受修改其捕获状态的闭包。因此,要求使用&Fn会对funTest调用者施加最大的限制,同时对f在其中如何被调用提供最小的限制。 - user4815162342

60

FnFnMutFnOnce是闭包类型,这些内容在其他答案中已经解释过。它们是那些闭合其作用域的函数类型。

除了传递闭包之外,Rust还支持传递简单(非闭包)函数,例如:

fn times2(value: i32) -> i32 {
    2 * value
}

fn fun_test(value: i32, f: fn(i32) -> i32) -> i32 {
    println!("{}", f (value));
    value
}

fn main() {
    fun_test (2, times2);
}

fn(i32) -> i32 这里是一个函数指针类型

如果您不需要一个完整的闭包,那么使用函数类型通常更简单,因为它不必处理那些闭包生命周期的细节。


这并不是我想要的 =) 找到了解决方法,返回动态闭包以捕获self的状态,因此我不需要传递实例引用... - Ivan Temchenko

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