Rust:传递异步函数指针

3

假设我们有以下代码:

fn inc(src: u32) -> u32 {
    src + 1
}

fn inc2(src: u32) -> u32 {
    src + 2
}

type Incrementer = fn(u32) -> u32;
fn increment_printer(inc: Incrementer) {
    println!("{}", inc(1));
}

fn main() {
    increment_printer(inc);
    increment_printer(inc2);
}

两个具有相同签名的函数和一个接受它们指针的第三个函数。运行此代码将导致打印 2\n3。

但类似的某些内容将无法编译:

use core::future::Future;

async fn inc(src: u32) -> u32 {
    src + 1
}

async fn inc2(src: u32) -> u32 {
    src + 2
}

type Incrementer = fn(u32) -> dyn Future<Output = u32>;
async fn increment_printer(inc: Incrementer) {
    println!("{}", inc(1).await);
}

fn main() {
    async {
        increment_printer(inc).await;
        increment_printer(inc2).await;
    }
}

18 |         increment_printer(inc).await;
   |                           ^^^ expected trait object `dyn Future`, found opaque type
   |
   = note: expected fn pointer `fn(_) -> (dyn Future<Output = u32> + 'static)`
                 found fn item `fn(_) -> impl Future {inc}`

我知道每个异步函数都有自己的类型,正如错误中所提到的。是否有可能强制编译器忘记具体类型并将它们视为相似类型?

也许可以强制异步函数返回一个 Box<Future<Output=u32>> 吗?

我不想为了方便而放弃异步函数。同时,我也不想在将异步函数指针传递给函数之前调用 Box::pin()。

如果有更多的选项,那就很有趣了。

3个回答

8

也许可以强制使异步函数返回Box<Future<Output=u32>>吗?

实际上不行,但是可以将函数封装到一个闭包中,该闭包调用函数并返回一个boxed future。当然,闭包本身需要被放在box中,以便像increment_printer这样的函数可以接收它,但两个boxing可以封装在一个实用函数中。例如 (playground):

use core::future::Future;
use std::pin::Pin;

async fn inc(src: u32) -> u32 {
    src + 1
}

async fn inc2(src: u32) -> u32 {
    src + 2
}

type Incrementer = Box<dyn FnOnce(u32) -> Pin<Box<dyn Future<Output = u32>>>>;

fn force_boxed<T>(f: fn(u32) -> T) -> Incrementer
where
    T: Future<Output = u32> + 'static,
{
    Box::new(move |n| Box::pin(f(n)))
}

async fn increment_printer(inc: Incrementer) {
    println!("{}", inc(1).await);
}

fn main() {
    async {
        increment_printer(force_boxed(inc)).await;
        increment_printer(force_boxed(inc2)).await;
    };
}

如果你能够将像increment_printer这样的函数泛化,那么就可以摆脱外部包装(playground)。
// emulate trait alias
trait Incrementer: FnOnce(u32) -> Pin<Box<dyn Future<Output = u32>>> {}
impl<T> Incrementer for T
    where T: FnOnce(u32) -> Pin<Box<dyn Future<Output = u32>>>
{
}

fn force_boxed<T>(f: fn(u32) -> T) -> impl Incrementer
where
    T: Future<Output = u32> + 'static,
{
    move |n| Box::pin(f(n)) as _
}

async fn increment_printer(inc: impl Incrementer) {
    println!("{}", inc(1).await);
}

我不想为了方便而放弃异步函数。同时,我也不想在将异步函数指针传递给函数之前调用Box::pin()
你需要在某个地方调用Box::pin()。通过上述方法,该调用至少被限制在一个地方,并且您可以获得类似于Incrementer类型的接收器,作为通过force_boxed()传递的异步函数的接收器。

1
它运行良好,API 也不错。但最好不要再做 FnOnce 舞蹈了。话虽如此,还是要感谢你,你给我省了很多时间。 - Dude
1
@伙计,很高兴能帮上忙 - 我现在已经编辑了答案,简化了 force_boxed() 的签名(它可以接受一个函数,而不是 FnOnce 本身),并提供了一种不需要将闭包框起来的替代方案,但代价是需要像 increment_printer 这样的函数是泛型的。 - user4815162342
2
这是一个基于这个答案的想法,它允许使用函数指针作为“Incrementer”,因此您不需要泛型(但您需要一个宏来根据需要包装“async fn”)。 [https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=8520a3bd55e7adeb5ebe2dbb293bef4a] - trent
1
@trentcl 不错!你可能想把它发表为答案。 - user4815162342
1
已完成。同时恭喜您获得10万声望! - trent
显示剩余2条评论

1
也许可以强制使一个异步函数返回一个 Box<Future<Output=u32>> ?您可以通过将其包装在普通(非 async)函数中来实现:
fn boxed_inc(src: u32) -> Pin<Box<dyn Future<Output = u32>>> {
    Box::pin(inc(src))
}
// Use like:
increment_printer(boxed_inc).await;

然而,如果你需要为所有需要包装的async函数编写boxed_incboxed_inc2等,那将很快变得单调乏味。这里有一个宏可以根据需要进行包装:
macro_rules! force_boxed {
    ($inc:expr) => {{
        // I think the error message referred to here is spurious, but why take a chance?
        fn rustc_complains_if_this_name_conflicts_with_the_environment_even_though_its_probably_fine(src: u32) -> Pin<Box<dyn Future<Output = u32>>> {
            Box::pin($inc(src))
        }
        rustc_complains_if_this_name_conflicts_with_the_environment_even_though_its_probably_fine
    }}
}
// Use like:
increment_printer(force_boxed!(inc)).await;

游乐场

这类似于并受到user4815162342的答案force_boxed函数的启发,但在这种情况下,它必须是一个宏,以便将结果强制转换为函数指针而不仅仅是闭包。


1

你可以通过特性系统实现相同的事情:

#[async_trait]
trait Incrementor {
    async fn increment(&self, src: u32) -> u32;
}

然后根据您的意愿实施不同的增量器。

#[async_trait]
impl Incrementor for Inc1 {
    async fn increment(&self, src: u32) -> u32 {
        src + 1
    }
}

#[derive(Default)]
struct Inc2;

#[async_trait]
impl Incrementor for Inc2 {
    async fn increment(&self, src: u32) -> u32 {
        src + 2
    }
}

这样做的好处是使用静态分派,如果你从一开始就控制对象的调用,如果不是的话,它仍然可以与 trait 对象一起工作。

游乐场


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