Rust中Box是否遮蔽trait bound?dyn Fn闭包赋值为何异常?
Rust 结构体Quox持有闭包的两类异常赋值行为成因
初始实现
计划定义名为Quox的结构体,支持可选持有三类Rust闭包(Fn、FnMut、FnOnce),初始结构体定义代码如下:
#[derive(Default, Debug)] pub struct Quox<Tfn: Fn(&Person) + Sized, Tfnmut: FnMut(&mut Person) + Sized, Tfnonce: FnOnce(&Person) + Sized> { soft_closure: Option<Tfn>, mut_closure : Option<Tfnmut>, move_closure: Option<Tfnonce>, }
实例化时选择用Box封装动态trait对象,触发原因是编译器报错the trait Sized is not implemented for dyn Fn()rustcE0277,实例化代码如下:
pub fn main(){ let mut qx = Quox::<Box<dyn Fn(&Person)>, Box<dyn FnMut(&mut Person)>, Box<dyn FnOnce(&Person)>> { move_closure: None, mut_closure : None, soft_closure: None, }; ... }
实现过程中遇到两处不符合预期的行为:
问题1:闭包可交叉赋值给不同trait bound的字段
创建闭包后,可交叉赋值给qx内不同字段且不触发编译错误,测试代码如下:
pub fn main(){ let mut qx = Quox::<Box<dyn Fn(&Person)>, Box<dyn FnMut(&mut Person)>, Box<dyn FnOnce(&Person)>> { move_closure: None, mut_closure : None, soft_closure: None, }; let greeting = "Hi. We are moving soon!".to_string(); let clsr_move = move |p:&Person| println!("Hi, {}. {} says: {}", p.name, p.name, greeting); let only_a_ref_clsr = |p:&Person| println!("Hi, {}.",p.name ); // qx.move_closure = Some(Box::new(clsr_move)); // 符合预期 // qx.soft_closure = Some(Box::new(only_a_ref_clsr)); // 符合预期 qx.soft_closure = Some(Box::new(clsr_move)); // 为何该赋值合法? qx.move_closure = Some(Box::new(only_a_ref_clsr)); // 已知Fn是FnOnce的supertrait,该行为可理解 }
核心困惑点:将看似FnMut/FnOnce类型的闭包赋值给要求Fn trait bound的字段时,编译器未抛出错误。
成因
Rust三个闭包trait的继承关系为 Fn: FnMut: FnOnce:
- 实现
Fn的闭包自动实现FnMut和FnOnce - 实现
FnMut的闭包自动实现FnOnce - 反向不成立
注意move关键字仅标记闭包捕获变量时的所有权转移规则,不决定闭包实现哪类闭包trait。示例中的clsr_move虽然用了move捕获greeting的所有权,但闭包体内仅对捕获值做不可变读取,没有修改、也没有消费捕获值,因此它同时实现了Fn、FnMut、FnOnce三个trait,完全满足soft_closure字段的Fn(&Person)约束,赋值自然合法。
只有闭包修改了捕获的可变变量时,才会仅实现FnMut + FnOnce;只有闭包消费了捕获值(如返回、drop捕获的所有权变量)时,才会仅实现FnOnce。
问题2:闭包存入中间变量后赋值触发类型不匹配错误
将封装闭包的Box先赋值给中间变量,再赋值给结构体字段时会触发编译错误,测试代码如下:
pub fn main(){ let mut qx = Quox::<Box<dyn Fn(&Person)>, Box<dyn FnMut(&mut Person)>, Box<dyn FnOnce(&Person)>> { move_closure: None, mut_closure : None, soft_closure: None, }; let greeting = "Hi. We are moving soon!".to_string(); let clsr_move = move |p:&Person| println!("Hi, {}. {} says: {}", p.name, p.name, greeting); let only_a_ref_clsr = |p:&Person| println!("Hi, {}.",p.name ); let someboxmove = Some(Box::new(only_a_ref_clsr)); qx.soft_closure = someboxmove; // 该赋值非法 }
触发的编译错误如下:
let someboxmove: Option<Box<|&Person| -> ()>> mismatched types expected enum `Option<Box<dyn for<'r> Fn(&'r traitobjects::Person)>>` found enum `Option<Box<[closure@src/traitobjects.rs:49:27: 49:66]>>`rustcE0308 traitobjects.rs(49, 27): the found closure traitobjects.rs(66, 5): expected due to the type of this binding
成因
该现象由Rust的类型推断与强转规则导致:
- 直接写
qx.soft_closure = Some(Box::new(only_a_ref_clsr))时,编译器已知赋值目标类型为Option<Box<dyn Fn(&Person)>>,会在明确的类型上下文下自动将Box<具体闭包匿名类型>做非固定大小强转(unsized coercion),转换为Box<dyn Fn(&Person)>动态trait对象类型,因此赋值合法。 - 先将值存入中间变量
someboxmove时,编译器没有强转的上下文提示,会直接将变量类型推断为Option<Box<[具体闭包匿名类型]>>。每个闭包的匿名类型都是独立的,和dyn Fn(&Person)动态trait对象类型不匹配,且Rust不会在跨变量赋值时自动触发这类强转,因此抛出类型错误。
Box本身不会遮蔽trait bound,动态trait对象的强转仅在有明确类型上下文时才会自动触发。如果要让中间变量写法合法,手动标注变量类型即可:
let someboxmove: Option<Box<dyn Fn(&Person)>> = Some(Box::new(only_a_ref_clsr)); qx.soft_closure = someboxmove; // 编译通过
实现优化建议
- 结构体定义中的
+ Sized约束是多余的,Rust泛型参数默认携带Sized约束,无需额外编写。 - 如果不需要结构体对闭包做静态分发,可以直接将三个字段的类型固定为对应的动态trait对象Box,无需保留泛型参数,简化实例化流程:
#[derive(Default, Debug)] pub struct Quox { soft_closure: Option<Box<dyn Fn(&Person)>>, mut_closure : Option<Box<dyn FnMut(&mut Person)>>, move_closure: Option<Box<dyn FnOnce(&Person)>>, }
该写法下实例化无需指定任何泛型参数,可直接初始化。
内容的提问来源于stack exchange,提问作者rtviii
相关产品推荐
相关产品推荐

