如何在Rust中实现类Python风格的上下文管理器?
我想在Rust里实现类似Python的上下文管理器。我有一个可读写的socket,还有持有这个socket的MyDevice结构体,用来通过socket给设备发命令:
pub struct MyDevice{ socket: MySocket, /* ... */ } impl MyDevice{ pub fn new()->MyDevice { todo!() } pub fn command(&mut self, s:&str)->std::io::Result<String>{ self.socket.write(s)?; self.socket.read() } /* ... */ }
常规操作示例:
let mut device = MyDevice::new(); device.command("Push setting A")?; device.command("Perform task 1")?; device.command("Pop setting A")?; device.command("Perform task 2")?; device.command("Push setting A")?; device.command("Perform task 3")?; device.command("Pop setting A")?; //<-- 注意setting A被多次加载和卸载
我想要的类似Python上下文管理器的伪代码如下:
// 我想要的伪代码 let setting_a : ContextManager = ContextManager::new( enter : ||{ device.command("Push setting A"); }, exit: || { device.command("Pop setting A"); } ) with setting_a { device.command("Perform task 1"); } device.command("Perform task 2"); with setting_a { device.command("Perform task 3"); } with setting_a{ device.command("Perform task 4"); with setting_b{ device.command("Perform task 5"); } device.command("Perform task 6"); }
我之前试过用Rc<RefCell<_>>实现类似功能,但处理不了需要执行子任务的上下文:
impl MyDevice{ /* ... */ pub fn with_param<F,O>(&mut self, param:Param,f:F)->O where F: Fn()->O { let last_param:Param = self.param.clone(); self.set_param(param); let o = f(); self.set_param(last_param); o } } device.with_param(param, ||{/* ... */}) // 伪代码示例:这种场景下with_param没法生效,因为进入和退出上下文时还需要执行额外子任务 let setting_a : ContextManager = ContextManager::new( enter : ||{ device.command("Push setting A"); device.command("Perform some task a-1"); }, exit: || { device.command("Pop setting A"); device.command("Perform some task a-2"); } )
现在有三个问题:
- 组织这类代码最合理的方式是什么?用传统的
Rc<RefCell<_>>还是宏? - 我希望实现panic安全,已知
RefCell可能引发panic,这种情况下能不能不用RefCell? - 如果未来需要在线程间传递这个上下文管理器,该怎么处理?还有如何实现异步安全?
问题1:代码组织方式
优先用闭包封装的方法,宏作为补充,尽量别碰Rc<RefCell<_>>。
你之前写的with_param思路是对的,只是需要扩展成更通用的上下文管理器模式。比如定义一个通用的ContextGuard结构体,在它的Drop trait里执行退出逻辑,然后给MyDevice加通用的上下文方法,或者针对特定场景写专用方法:
// 通用上下文守卫,负责在drop时执行退出逻辑 struct ContextGuard<F: FnOnce() -> std::io::Result<()>> { exit: F, } impl<F: FnOnce() -> std::io::Result<()>> Drop for ContextGuard<F> { fn drop(&mut self) { // 可根据需求处理退出错误,这里暂时忽略 let _ = (self.exit)(); } } impl MyDevice { // 通用的上下文进入方法,返回守卫 pub fn enter_context<Enter, Exit>(&mut self, enter: Enter, exit: Exit) -> std::io::Result<ContextGuard<Exit>> where Enter: FnOnce(&mut Self) -> std::io::Result<()>, Exit: FnOnce() -> std::io::Result<()>, { enter(self)?; Ok(ContextGuard { exit }) } // 针对Setting A的专用方法,更易用 pub fn with_setting_a<F, O>(&mut self, f: F) -> std::io::Result<O> where F: FnOnce(&mut Self) -> std::io::Result<O>, { // 进入上下文 self.command("Push setting A")?; self.command("Perform some task a-1")?; // 创建守卫,确保退出逻辑一定会执行 let guard = ContextGuard { exit: || { self.command("Pop setting A")?; self.command("Perform some task a-2") } }; let result = f(self); // guard会在这里自动drop,执行退出逻辑 result } }
如果需要复用上下文逻辑,写专用方法比宏更安全,类型检查更严格。宏的调试成本高,只有在非常通用的场景下再考虑使用。Rc<RefCell<_>>会引入运行时开销和借用检查的不确定性,除非你真的需要共享可变状态,否则完全没必要用。
问题2:panic安全与避免RefCell
完全可以不用RefCell,而且必须避免用它来保证panic安全。
刚才的ContextGuard模式就是panic安全的:只要ContextGuard被正确创建,不管闭包里有没有panic,Drop方法都会被调用,退出逻辑一定会执行。这里的关键是不要把MyDevice的可变引用放到闭包之外的共享容器里,而是通过方法传递可变引用,让编译器帮你做借用检查,从根源上避免RefCell的运行时panic。
另外要注意退出逻辑本身的panic:如果exit方法panic,会覆盖原来的panic信息,所以可以在Drop里用std::panic::catch_unwind包裹(仅适用于UnwindSafe的代码),或者把错误存储下来让用户后续处理。
问题3:线程间传递与异步安全
线程间传递
如果要在线程间传递MyDevice和上下文管理器,首先MyDevice必须实现Send和Sync:
MySocket要实现Send和Sync(大部分标准库的socket类型都满足);- 其他成员也要满足
Send和Sync。
然后用Arc<Mutex<MyDevice>>(普通线程)或者Arc<RwLock<MyDevice>>(多读少写场景)实现线程安全的共享,上下文方法可以改成:
use std::sync::{Arc, Mutex}; struct ThreadSafeContextGuard<F: FnOnce() -> std::io::Result<()>> { exit: F, } impl<F: FnOnce() -> std::io::Result<()>> Drop for ThreadSafeContextGuard<F> { fn drop(&mut self) { let _ = (self.exit)(); } } impl MyDevice { pub fn with_setting_a_thread_safe(arc_self: Arc<Mutex<Self>>) -> std::io::Result<ThreadSafeContextGuard<impl FnOnce() -> std::io::Result<()>>> { let mut guard = arc_self.lock().map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; guard.command("Push setting A")?; guard.command("Perform some task a-1")?; Ok(ThreadSafeContextGuard { exit: move || { let mut guard = arc_self.lock().map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; guard.command("Pop setting A")?; guard.command("Perform some task a-2") } }) } }
异步安全
异步场景下,要用tokio::sync::Mutex或者async_std::sync::Mutex代替标准库的Mutex(标准库Mutex会阻塞线程,不适合异步)。同时,上下文管理器的进入和退出逻辑要改成异步方法:
use tokio::sync::{Arc, Mutex}; use std::future::Future; struct AsyncContextGuard<F> where F: FnOnce() -> std::pin::Pin<Box<dyn Future<Output = std::io::Result<()>> + Send>>, { exit: F, } impl<F> Drop for AsyncContextGuard<F> where F: FnOnce() -> std::pin::Pin<Box<dyn Future<Output = std::io::Result<()>> + Send>>, { fn drop(&mut self) { // 异步代码不能在Drop里直接执行,这里用spawn把退出逻辑放到异步任务中 tokio::spawn((self.exit)()); } } impl MyDevice { pub async fn with_setting_a_async(arc_self: Arc<Mutex<Self>>) -> std::io::Result<AsyncContextGuard<impl FnOnce() -> std::pin::Pin<Box<dyn Future<Output = std::io::Result<()>> + Send>>>> { let mut guard = arc_self.lock().await; guard.command("Push setting A")?; guard.command("Perform some task a-1")?; Ok(AsyncContextGuard { exit: move || { let arc_self = arc_self.clone(); Box::pin(async move { let mut guard = arc_self.lock().await; guard.command("Pop setting A")?; guard.command("Perform some task a-2") }) } }) } }
注意:异步场景下的Drop处理比较麻烦,因为Drop是同步方法。如果需要严格保证退出逻辑执行,最好让用户显式调用异步的退出方法,或者使用专门的异步drop库(比如async-drop)。
内容的提问来源于stack exchange,提问作者啊鹿Dizzyi

