Rust中如何在循环内用简洁静态分派处理首迭代特殊类型场景?
问题:简化带特殊初始类型的循环代码
我有一个for循环,其中第一次迭代是特殊情况——唯一的区别是某个变量的类型,尽管这两个类型都实现了相同的trait。我想知道是否有办法以及如何让代码更简洁。
存在一个具体类型Concrete,它为多种T实现了带有foo<T>(&mut self, modifier: T)方法的trait FooTrait<T>。值得注意的是,FooTrait<&Concrete>已被实现。
原始非泛型代码
pub fn bar(slice_len, initial: &Concrete) { let concrete_slice: [Concrete; slice_len] = initialize_slice(); let mut carry: &Concrete = initial; for i in 0..concrete_slice.len() { concrete_slice[i].foo(carry); carry = &concrete_slice[i]; } epilogue(carry); }
泛化后(但不够简洁)的代码
pub fn bar<T>(slice_len, initial: T) where Concrete: FooTrait<T> { let concrete_slice: [Concrete; slice_len] = initialize_slice(); if slice_len > 0 { concrete_slice[0].foo(initial); let mut carry: &Concrete = &concrete_slice[0]; for i in 1..concrete_slice.len() { concrete_slice[i].foo(carry); carry = &concrete_slice[i]; } epilogue(carry); } else { epilogue(initial); } }
显然这是一个经过泛化和简化的示例,我的实际代码更复杂。为了可读性,我希望代码更接近第一个版本而非第二个。我知道可以用enum包装多种不同类型,但我不知道是否有一种方式能使用enum且不降低代码简洁性。我也知道trait object的存在,但它们会带来运行时开销。是否有更好的惯用写法?
解决方案
1. 自定义极简枚举(零开销)
自己定义一个枚举包装初始值和引用,用迭代器的fold方法统一处理循环逻辑,消除分支拆分,代码逻辑和原始循环高度一致:
pub fn bar<T>(slice_len: usize, initial: T) where Concrete: FooTrait<T> + FooTrait<&'_ Concrete>, { let mut concrete_slice: [Concrete; slice_len] = initialize_slice(); let carry = concrete_slice.iter_mut() .enumerate() .fold(InitialOrRef::Initial(initial), |carry, (i, elem)| { match carry { InitialOrRef::Initial(val) => { elem.foo(val); InitialOrRef::Ref(&concrete_slice[i]) } InitialOrRef::Ref(c) => { elem.foo(c); InitialOrRef::Ref(&concrete_slice[i]) } } }); match carry { InitialOrRef::Initial(val) => epilogue(val), InitialOrRef::Ref(c) => epilogue(c), } } // 仅用于包装两种类型的极简枚举,无额外运行时开销 enum InitialOrRef<T> { Initial(T), Ref(&'_ Concrete), }
这个枚举是Rust零开销抽象的典型应用,编译器会优化掉多余的标签开销,性能和手写分支完全一致,同时代码结构更连贯。
2. 复用社区成熟的Either枚举
如果不想自己定义枚举,可以使用either crate中的Either类型,它已经封装了双类型的包装逻辑,代码更简洁:
use either::Either; pub fn bar<T>(slice_len: usize, initial: T) where Concrete: FooTrait<T> + FooTrait<&'_ Concrete>, { let mut concrete_slice: [Concrete; slice_len] = initialize_slice(); let carry = concrete_slice.iter_mut() .enumerate() .fold(Either::Left(initial), |carry, (i, elem)| { match carry { Either::Left(val) => { elem.foo(val); Either::Right(&concrete_slice[i]) } Either::Right(c) => { elem.foo(c); Either::Right(&concrete_slice[i]) } } }); match carry { Either::Left(val) => epilogue(val), Either::Right(c) => epilogue(c), } }
3. 泛型适配器(适合复杂场景)
如果需要更灵活的抽象,可以定义一个trait来统一两种输入类型的行为,不过这种方式适合逻辑更复杂的场景,简单情况没必要:
// 抽象可传递给foo的输入类型 trait FooInput { fn apply(self, elem: &mut Concrete); fn into_epilogue(self) -> EpilogueInput; // 根据实际epilogue参数调整 } // 为初始类型实现适配器 impl<T> FooInput for T where Concrete: FooTrait<T>, { fn apply(self, elem: &mut Concrete) { elem.foo(self); } fn into_epilogue(self) -> EpilogueInput { EpilogueInput::Initial(self) } } // 为引用类型实现适配器 impl FooInput for &Concrete { fn apply(self, elem: &mut Concrete) { elem.foo(self); } fn into_epilogue(self) -> EpilogueInput { EpilogueInput::Ref(self) } } // 假设epilogue接受的参数类型 enum EpilogueInput { Initial(/* 对应初始T类型 */), Ref(&'_ Concrete), } pub fn bar<T: FooInput>(slice_len: usize, initial: T) { let mut concrete_slice: [Concrete; slice_len] = initialize_slice(); let carry = concrete_slice.iter_mut() .fold(InitialOrRef::Initial(initial), |carry, elem| { match carry { InitialOrRef::Initial(val) => { val.apply(elem); InitialOrRef::Ref(elem) } InitialOrRef::Ref(c) => { c.apply(elem); InitialOrRef::Ref(elem) } } }); epilogue(carry.into_epilogue()); }
内容的提问来源于stack exchange,提问作者Nic
相关产品推荐
相关产品推荐

