你能克隆一个闭包吗?

25

一个 FnMut 闭包由于明显的原因无法进行克隆,但是一个 Fn 闭包具有不可变的作用域;是否有某种方式可以创建一个 Fn 闭包的“副本”?

尝试克隆它会导致以下错误:

error[E0599]: no method named `clone` found for type `std::boxed::Box<std::ops::Fn(i8, i8) -> i8 + std::marker::Send + 'static>` in the current scope
  --> src/main.rs:22:25
   |
22 |             fp: self.fp.clone(),
   |                         ^^^^^
   |
   = note: self.fp is a function, perhaps you wish to call it
   = note: the method `clone` exists but the following trait bounds were not satisfied:
           `std::boxed::Box<std::ops::Fn(i8, i8) -> i8 + std::marker::Send> : std::clone::Clone`

将裸指针传递给Fn是否安全,例如:

let func_pnt = &mut Box<Fn<...> + Send> as *mut Box<Fn<...>>

从技术上讲,以上方法可以行得通,但看起来相当奇怪。

以下是我正在尝试的示例:

use std::thread;

struct WithCall {
    fp: Box<Fn(i8, i8) -> i8 + Send>,
}

impl WithCall {
    pub fn new(fp: Box<Fn(i8, i8) -> i8 + Send>) -> WithCall {
        WithCall { fp: fp }
    }

    pub fn run(&self, a: i8, b: i8) -> i8 {
        (self.fp)(a, b)
    }
}

impl Clone for WithCall {
    fn clone(&self) -> WithCall {
        WithCall {
            fp: self.fp.clone(),
        }
    }
}

fn main() {
    let adder = WithCall::new(Box::new(|a, b| a + b));
    println!("{}", adder.run(1, 2));

    let add_a = adder.clone();
    let add_b = adder.clone();

    let a = thread::spawn(move || {
        println!("In remote thread: {}", add_a.run(10, 10));
    });

    let b = thread::spawn(move || {
        println!("In remote thread: {}", add_b.run(10, 10));
    });

    a.join().expect("Thread A panicked");
    b.join().expect("Thread B panicked");
}

游乐场

我有一个在结构体中的闭包,我需要将该结构体传递给多个线程。但我不能直接传递,也不能克隆它,因为无法克隆Box<Fn<>>,也无法克隆&Fn<...>


您想对克隆的闭包做什么? - Shepmaster
你的完整代码是什么? - huon
@shepmaster 我特别想克隆一个没有可变状态的闭包,以便将其同时移动到多个任务中。请参见我附加的示例。 - Doug
1
无法克隆 &Fn<...>。你可以,特别是 ?Sized 表示它可以与 trait 对象一起使用。 - huon
@dbaupp Box::new(*(&(*self.fp)).clone()) -> 错误:类型 core::ops::Fn(i8, i8) -> i8 + Send 没有实现 trait core::marker::Sized - Doug
这是由于发生了所有的取消引用,而不是 &Fn() 克隆直接作用。我预计,没有 .clone() 也会出现相同的错误消息。 - huon
3个回答

14

Rust 1.26

如果闭包捕获的所有变量都支持CopyClone,则闭包本身也会实现这两个特性。你可以重写你的代码,使用泛型代替盒式 trait 对象,以便能够克隆它:

use std::thread;

#[derive(Clone)]
struct WithCall<F> {
    fp: F,
}

impl<F> WithCall<F>
where
    F: Fn(i8, i8) -> i8,
{
    pub fn new(fp: F) -> Self {
        WithCall { fp }
    }

    pub fn run(&self, a: i8, b: i8) -> i8 {
        (self.fp)(a, b)
    }
}

fn main() {
    let adder = WithCall::new(|a, b| a + b);
    println!("{}", adder.run(1, 2));

    let add_a = adder.clone();
    let add_b = adder;

    let a = thread::spawn(move || {
        println!("In remote thread: {}", add_a.run(10, 10));
    });

    let b = thread::spawn(move || {
        println!("In remote thread: {}", add_b.run(10, 10));
    });

    a.join().expect("Thread A panicked");
    b.join().expect("Thread B panicked");
}

在Rust 1.26之前

请记住,闭包会捕获它们的环境,因此它们有自己的生命周期,基于环境。但是,您可以获取对Fn*的引用,并将其进一步传递或存储在结构体中:

fn do_more<F>(f: &F) -> u8
where
    F: Fn(u8) -> u8,
{
    f(0)
}

fn do_things<F>(f: F) -> u8
where
    F: Fn(u8) -> u8,
{
    // We can pass the reference to our closure around,
    // effectively allowing us to use it multiple times.
    f(do_more(&f))
}

fn main() {
    let val = 2;
    // The closure captures `val`, so it cannot live beyond that.
    println!("{:?}", do_things(|x| (x + 1) * val));
}

我认为把Fn*转换成裸指针并传递给其他函数不是完全安全的,因为存在生命周期方面的问题。


1
Shep,“WithCall” 实际上没有做任何事情;你可以在不使用它的情况下进行克隆。没有什么可以阻止 F: Fn(i8) -> i8 支持 Clone。但是尝试克隆一个 Box<Fn(i8) -> i8>;我 不认为 这是(安全地)可能的! - dhardy
2
@dhardy 是的,这不是必需的,但它存在是为了反映OP原始结构,其中他们有一个包含闭包而不仅仅是闭包的类型。你可以在它上面derive(Clone)表明闭包是可克隆的,这也是我第一句话试图传达的:*如果所有捕获的变量都是,则闭包实现CopyClone*。而且,你不能克隆一个盒装特质对象,因为Clone不是对象安全的 - Shepmaster
1
那么,无法复制 Fn(...) 特质对象吗?当然,&Fn(...) 是可复制的,但这带有生命周期限制。 - dhardy

12
你正在尝试从多个线程调用闭包,也就是在多个线程之间共享闭包。一旦想到“跨多个线程共享”,我的第一个想法就是使用 Arc(至少在实现RFC 458的某种形式之前,当&可以在不同线程之间使用时)。
这允许安全共享内存(它实现了Clone而不需要其内部类型为Clone,因为Clone只是创建指向相同内存的新指针),因此您可以有一个单独的Fn对象在多个线程中使用,无需复制它。
总之,将你的WithCall放入一个Arc中并进行克隆即可。
use std::sync::Arc;
use std::thread;

type Fp = Box<Fn(i8, i8) -> i8 + Send + Sync>;

struct WithCall {
    fp: Fp,
}

impl WithCall {
    pub fn new(fp: Fp) -> WithCall {
        WithCall { fp }
    }

    pub fn run(&self, a: i8, b: i8) -> i8 {
        (self.fp)(a, b)
    }
}

fn main() {
    let adder = WithCall::new(Box::new(|a, b| a + b));
    println!("{}", adder.run(1, 2));

    let add_a = Arc::new(adder);
    let add_b = add_a.clone();

    let a = thread::spawn(move || {
        println!("In remote thread: {}", add_a.run(10, 10));
    });
    let b = thread::spawn(move || {
        println!("In remote thread: {}", add_b.run(10, 10));
    });

    a.join().expect("thread a panicked");
    b.join().expect("thread b panicked");
}

游乐场


旧回答(仍然相关):很少见到有 &mut Fn trait object 的情况,因为Fn::call需要传入 &selfmut 不是必需的,我认为它没有添加任何额外的功能。使用 &mut Box<Fn()> 会添加一些功能,但这也是不寻常的。

如果将指针改为 & 而不是 &mut,事情会更自然地进行(包括 &Fn&Box<Fn>)。 如果您不看到实际使用的代码,则极难确切了解您正在做什么,但

fn call_it(f: &Fn()) {
    (*f)();
    (*f)();
}

fn use_closure(f: &Fn()) {
    call_it(f);
    call_it(f);
}

fn main() {
    let x = 1i32;
    use_closure(&|| println!("x is {}", x));
}

这在一定程度上是由于&TCopy,以及部分原因是由于重新借用; 它也适用于&mut

或者,您可以闭合闭包,这样可能在更多情况下起作用:

fn foo(f: &Fn()) {
    something_else(|| f())
}

一个 FnMut 闭包由于一些显而易见的原因是不能被克隆的。

事实上,一个 FnMut 是可以被克隆的,它只是一个带有一些字段的结构体(和一个使用 &mut self 而不是 &self 或者 self 的方法,分别用于 FnFnOnce)。如果你创建一个结构体并手动实现了 FnMut,那么你仍然可以为它实现 Clone

Or is it safe to somehow pass a raw pointer to a Fn around, like:

let func_pnt = &mut Box<Fn<...> + Send> as *mut Box<Fn<...>>

Technically the above works, but it seems quite weird.

从技术上讲,如果您小心确保 Rust 的别名和生命周期要求得到满足,那么使用 unsafe 指针是可行的...但通过选择使用 unsafe 指针,您将把这个负担放在自己身上,而不是让编译器帮助您。正确的响应编译器错误的情况下需要使用 unsafe 代码相对较少,而是深入了解错误并调整代码使其更加合理(对于编译器来说更加合理,通常也会使人类更容易理解)。


抱歉,那个问题有点儿糟糕。我已经更新了一个示例,展示了我想要做的事情。在闭包中再次引用另一个闭包可能是一种解决方案,但我不确定如何实现。 - Doug
@Doug,是的,没有实际来源的问题很难以有用的方式回答。已更新。 - huon
@dhardy huon并不是说你可以克隆一个&mut T,而是你可以克隆一个包含接受&mut T函数和一个接受&mut self方法的结构体。 - Shepmaster
@dhardy 没有必要使用Arc — 这已经在如何将对堆栈变量的引用传递给线程?中讨论过了。对于std::thread,确实需要使用Arc,因为所有权被转移给了线程。 - Shepmaster
@Shepmaster,我原本以为克隆函数可能会在相同的数据上调用,但是当然克隆FnMut也会克隆数据,所以huon是正确的。(我删除了之前的评论,因为SO现在不让我更正了,在这里留下错误的信息也没有必要。) - dhardy

0

这是在1.22.1中的可用代码。

目的是使其正常工作。

let x = |x| { println!("----{}",x)};

let mut y = Box::new(x);

y.clone();

我使用了顶部建议的原始代码。

我从克隆一个Fn闭包开始。

type Fp = Box<Fn(i8, i8) -> i8 + Send + Sync>;

最终在结构体WithCall中添加了Arc来包装Fp

Play rust : 工作代码 Gist : 1.22.1版本的工作代码


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