Rust中用From/Into实现默认参数时如何消除代码冗余
我参考此前Stack Overflow上关于Rust默认函数参数的相关提问,尝试采用「参数结构体+From/Into trait」的模式实现默认参数能力,扩展该模式的适用场景。
但当前实现存在明显的代码冗余问题:每新增一个类似BarArg、BazArg的参数结构体,都需要重复实现Default trait,以及针对空元组、单个参数、多参数元组的From转换逻辑;后续为Foo结构体新增关联函数时,也需要对应编写大量重复的From/Into trait实现代码,维护成本较高。
示例代码如下:
type FileName = Option<String>; pub struct BarArg { a: f64, b: FileName, } impl Default for BarArg { fn default() -> Self { BarArg { a: 1.0, b: None } } } impl From<()> for BarArg { fn from(_: ()) -> Self { Self::default() } } impl From<f64> for BarArg { fn from(a: f64) -> Self { Self { a, ..Self::default() } } } impl From<Option<String>> for BarArg { fn from(b: Option<String>) -> Self { Self { b, ..Self::default() } } } impl From<(f64, Option<String>)> for BarArg { fn from((a, b): (f64, Option<String>)) -> Self { Self { a, b } } } pub struct BazArg { a: i32, b: FileName, } impl Default for BazArg { fn default() -> Self { BazArg { a: 1, b: None } } } impl From<()> for BazArg { fn from(_: ()) -> Self { Self::default() } } impl From<i32> for BazArg { fn from(a: i32) -> Self { Self { a, ..Self::default() } } } impl From<Option<String>> for BazArg { fn from(b: Option<String>) -> Self { Self { b, ..Self::default() } } } impl From<(i32, Option<String>)> for BazArg { fn from((a, b): (i32, Option<String>)) -> Self { Self { a, b } } } struct Foo; impl Foo { pub fn bar<A>(bar_arg: A) where A: Into<BarArg>, { match bar_arg.into().b { Some(b) => { println!("FileName is: {:?}", b); } None => { println!("No FileName was passed"); } }; } pub fn baz<A>(baz_arg: A) where A: Into<BazArg>, { match baz_arg.into().b { Some(b) => { println!("FileName is: {:?}", b); } None => { println!("No FileName was passed"); } }; } } fn main() { Foo::bar(()); Foo::bar(Some("bar.toml".to_string())); Foo::baz(()); Foo::baz(Some("baz.toml".to_string())); }
核心诉求:是否存在非冗余、简洁的实现方式,能够在保留该默认参数模式能力的前提下,消除重复样板代码?
以下两种方案均可完全保留原有调用能力,同时消除绝大多数重复样板代码:
方案1:派生宏自动生成转换实现
使用derive_more库提供的派生宏,可自动为结构体生成Default、From等trait的实现,无需手动编写重复逻辑。
首先添加依赖:
[dependencies] derive_more = { version = "1.0", features = ["from", "default"] }
改写参数结构体即可,所有转换逻辑自动生成,原有业务代码无需修改:
use derive_more::{Default, From}; type FileName = Option<String>; #[derive(Default, From)] pub struct BarArg { a: f64, b: FileName, } #[derive(Default, From)] pub struct BazArg { a: i32, b: FileName, } struct Foo; impl Foo { pub fn bar<A>(bar_arg: A) where A: Into<BarArg>, { match bar_arg.into().b { Some(b) => println!("FileName is: {:?}", b), None => println!("No FileName was passed"), }; } pub fn baz<A>(baz_arg: A) where A: Into<BazArg>, { match baz_arg.into().b { Some(b) => println!("FileName is: {:?}", b), None => println!("No FileName was passed"), }; } } fn main() { // 原有调用方式完全兼容 Foo::bar(()); Foo::bar(2.5); Foo::bar(Some("bar.toml".to_string())); Foo::bar((2.5, Some("bar.toml".to_string()))); Foo::baz(()); Foo::baz(42); Foo::baz(Some("baz.toml".to_string())); Foo::baz((42, Some("baz.toml".to_string()))); }
说明:derive_more的From派生默认会为每个字段的类型生成单参数From实现,同时为和结构体字段顺序一致的元组生成全量参数From实现,配合Default派生覆盖空元组转换场景,完全匹配手写实现的所有能力。
方案2:Builder模式(无第三方依赖)
如果不想引入外部依赖,可使用Rust生态通用的Builder模式实现默认参数,同样可以消除冗余,且调用可读性更强:
type FileName = Option<String>; #[derive(Clone)] pub struct BarArg { a: f64, b: FileName, } impl Default for BarArg { fn default() -> Self { Self { a: 1.0, b: None } } } impl BarArg { pub fn new() -> Self { Self::default() } pub fn a(mut self, a: f64) -> Self { self.a = a; self } pub fn b(mut self, b: impl Into<FileName>) -> Self { self.b = b.into(); self } } #[derive(Clone)] pub struct BazArg { a: i32, b: FileName, } impl Default for BazArg { fn default() -> Self { Self { a: 1, b: None } } } impl BazArg { pub fn new() -> Self { Self::default() } pub fn a(mut self, a: i32) -> Self { self.a = a; self } pub fn b(mut self, b: impl Into<FileName>) -> Self { self.b = b.into(); self } } // 仅需实现少量通用From即可兼容原有元组传参习惯 impl From<()> for BarArg { fn from(_: ()) -> Self { Self::default() } } impl From<f64> for BarArg { fn from(a: f64) -> Self { Self::default().a(a) } } impl From<FileName> for BarArg { fn from(b: FileName) -> Self { Self::default().b(b) } } impl From<(f64, FileName)> for BarArg { fn from((a,b): (f64, FileName)) -> Self { Self::default().a(a).b(b) } } impl From<()> for BazArg { fn from(_: ()) -> Self { Self::default() } } impl From<i32> for BazArg { fn from(a: i32) -> Self { Self::default().a(a) } } impl From<FileName> for BazArg { fn from(b: FileName) -> Self { Self::default().b(b) } } impl From<(i32, FileName)> for BazArg { fn from((a,b): (i32, FileName)) -> Self { Self::default().a(a).b(b) } } struct Foo; impl Foo { pub fn bar(bar_arg: impl Into<BarArg>) { let arg = bar_arg.into(); match arg.b { Some(b) => println!("FileName is: {:?}", b), None => println!("No FileName was passed"), } } pub fn baz(baz_arg: impl Into<BazArg>) { let arg = baz_arg.into(); match arg.b { Some(b) => println!("FileName is: {:?}", b), None => println!("No FileName was passed"), } } } fn main() { // 原有调用方式兼容 Foo::bar(()); Foo::bar(Some("bar.toml".to_string())); // 支持链式调用,参数顺序不受限制,可读性更高 Foo::bar(BarArg::new().a(3.14).b("bar2.toml".to_string())); Foo::baz(BazArg::new().b("baz2.toml".to_string()).a(100)); }
如果需要进一步减少样板代码,也可以使用derive_builder库自动生成Builder的所有setter方法,无需手动编写。
选型参考
- 需要最小改动、完全保留原有元组传参习惯:优先选择派生宏方案,代码量最少,改造成本最低
- 不希望引入第三方依赖、追求参数可读性:选择原生Builder模式,链式调用不受参数顺序限制,长期维护成本更低
内容的提问来源于stack exchange,提问作者user76333

