如何编写可便捷接收函数列表的并发API?
兄弟,我太懂你这种想写出像C那样简洁调用的并发函数的需求了!在Rust里直接用&[&dyn Fn()]确实会踩坑——毕竟每个闭包都是独有的匿名类型,编译器根本没法把它们塞进同一个数组里,也不会自动帮你转成 trait 对象,这和C的std::function隐式转换不一样。不过别担心,有几个办法能帮你实现近似的便捷调用,我给你详细说下:
方案一:用Vec<Box<dyn Fn()>>直接接收
这是最直接的思路,把每个闭包装箱成 trait 对象,因为Box<dyn Fn()>是大小固定的类型,能顺利放进Vec里。代码大概是这样:
fn fan_out(fns: Vec<Box<dyn Fn()>>) { let mut thread_handles = vec![]; // 逐个启动线程执行函数 for func in fns { thread_handles.push(std::thread::spawn(move || func())); } // 等待所有线程完成 for handle in thread_handles { handle.join().unwrap(); } } // 调用的时候给每个闭包套个Box::new fan_out(vec![ Box::new(|| println!("taco")), Box::new(|| println!("burrito")), ]);
这个方案的缺点是每次调用都要手动写Box::new,有点啰嗦,但胜在简单直接,容易理解。
方案二:用宏简化调用(最接近你想要的写法)
如果想完全去掉Box::new的冗余代码,写个宏来帮我们自动处理就好!宏可以在编译期把每个传入的闭包自动包裹成Box::new,然后收集成Vec传给实际的实现函数:
// 实际的逻辑实现函数 fn fan_out_impl(fns: Vec<Box<dyn Fn()>>) { let mut thread_handles = vec![]; for func in fns { thread_handles.push(std::thread::spawn(move || func())); } for handle in thread_handles { handle.join().unwrap(); } } // 定义宏来简化调用 macro_rules! fan_out { ($($func:expr),* $(,)?) => { fan_out_impl(vec![$(Box::new($func)),*]) }; } // 现在调用起来就和你想要的一模一样了! fan_out!( || println!("taco"), || println!("burrito"), );
这个宏还支持最后加个逗号的写法(就是fan_out!(|| println!("taco"), || println!("burrito"),);),更符合Rust的编码习惯,用起来和C++的写法几乎没差别。
方案三:泛型可变参数实现(静态分发,无额外开销)
如果追求极致性能,不想用 trait 对象带来的动态分发开销,可以用递归泛型trait来实现可变参数的调用。这种方案里每个闭包都保持自己的具体类型,完全是静态编译:
use std::thread; // 对外暴露的入口函数,接收第一个闭包和剩余的参数 fn fan_out<F, Rest>(first_func: F, rest: Rest) where F: FnOnce() + Send + 'static, Rest: FanOut, { let handle = thread::spawn(first_func); rest.fan_out(); handle.join().unwrap(); } // 定义一个trait来处理剩余的参数 trait FanOut { fn fan_out(self); } // 终止条件:没有剩余参数时什么都不做 impl FanOut for () { fn fan_out(self) {} } // 递归处理剩余的每个参数 impl<F, Rest> FanOut for (F, Rest) where F: FnOnce() + Send + 'static, Rest: FanOut, { fn fan_out(self) { let handle = thread::spawn(self.0); self.1.fan_out(); handle.join().unwrap(); } } // 调用方式也很简洁,直接传多个闭包就行 fan_out( || println!("taco"), || println!("burrito"), );
这个方案的优点是完全没有动态分发的开销,缺点是实现起来稍微复杂一点,而且参数是逐个传递的,不是放在数组/vec里,但调用体验依然很流畅。
总结一下:如果想要最接近你预期的简洁调用,宏的方案是首选;如果追求性能,泛型可变参数的方案更合适;如果能接受一点点代码冗余,直接用Vec<Box<dyn Fn()>>也完全没问题。
内容来源于stack exchange




