Rust中如何在编译期检测结构体所需字段是否已填充?
如何在Rust中通过编译期检查避免未填充字段的
expect调用? 我在Rust应用中使用以下结构体:
pub struct Invoice { pub id: Uuid, pub amount: i64, pub rows: Vec<InvoiceRow>, pub company: Option<Box<Company>>, pub customer: Option<Box<Customer>>, pub payments: Vec<Payment>, // 其他大量字段 } pub struct Payment { pub id: Uuid, pub amount: i64, pub invoice_id: Uuid, pub payment_method_id: Uuid, pub payment_method: Option<Box<PaymentMethod>>, // 其他大量字段 } pub struct PaymentMethod { pub id: Uuid, pub name: String, pub mode: String, // 其他大量字段 }
我编写了如下方法,其中不得不使用expect来强制解包Option:
fn payments(invoice: &Invoice) -> String { let payments = invoice .payments .iter() .fold(String::new(), |mut output, payment| { let _ = write!( output, "mode: {mode} - amount: {amount}\n", mode = payment.payment_method.as_ref().expect("payment_method").mode(), amount = payment.amount(), ); output }); payments }
Invoice实例由数据库调用生成,通过以下结构体指定要查询的字段:
pub struct InvoiceFieldsToQuery { pub rows: Option<Box<InvoiceRowFieldsToQuery>>, pub company: Option<Box<CompanyFieldsToQuery>>, pub customer: Option<Box<CustomerFieldsToQuery>>, pub payments: Option<Box<PaymentFieldsToQuery>>, // 其他大量字段 } let fields_to_query = InvoiceFieldsToQuery::default() .query_company(Some(Box::new( CompanyFieldsToQuery::default().query_address(), ))) .query_customer(Some(Box::new( CustomerFieldsToQuery::default().query_address(), ))) .query_payments(Some(Box::new( PaymentFieldsToQuery::default()) ));
我的需求是:
- 避免使用
expect,通过编译期检查确保字段已填充 - 理想情况下,当代码需要使用
payments或payment_method时,编译期能强制要求必须通过query_payments指定查询该字段 - 字段必须保留按需加载的特性——只在需要时才查询,不需要时不加载
解决方案:利用Rust类型系统实现编译期状态检查
核心思路是用类型状态模式(Type State Pattern),通过标记类型区分结构体字段的"已加载"和"未加载"状态,让编译器强制约束方法只能接收已加载对应字段的结构体实例。
步骤1:定义状态标记类型
创建空的标记类型,用来标识字段的加载状态:
// 标记字段未加载 pub struct Unloaded; // 标记字段已加载 pub struct Loaded;
步骤2:重构结构体为泛型,关联状态标记
将需要动态加载的字段用泛型参数绑定状态,让字段类型随状态变化(未加载为Option,已加载为确定类型):
use uuid::Uuid; // 为Payment的payment_method字段定义状态trait pub trait PaymentMethodState { type Type; } impl PaymentMethodState for Unloaded { type Type = Option<Box<PaymentMethod>>; } impl PaymentMethodState for Loaded { type Type = Box<PaymentMethod>; } // 泛型Payment结构体 pub struct Payment<P: PaymentMethodState> { pub id: Uuid, pub amount: i64, pub invoice_id: Uuid, pub payment_method_id: Uuid, pub payment_method: P::Type, } // 为Invoice的payments字段定义状态trait pub trait PaymentsState { type Type; } impl PaymentsState for Unloaded { type Type = Vec<Payment<Unloaded>>; } impl PaymentsState for Loaded { type Type = Vec<Payment<Loaded>>; } // 泛型Invoice结构体 pub struct Invoice<P: PaymentsState> { pub id: Uuid, pub amount: i64, pub rows: Vec<InvoiceRow>, pub company: Option<Box<Company>>, pub customer: Option<Box<Customer>>, pub payments: P::Type, } // 其他基础结构体保持原样 pub struct InvoiceRow; pub struct Company; pub struct Customer; pub struct PaymentMethod { pub id: Uuid, pub name: String, pub mode: String, }
步骤3:关联查询配置与结构体状态
修改数据库查询逻辑,根据InvoiceFieldsToQuery的配置返回对应状态的Invoice实例:
pub struct InvoiceFieldsToQuery { pub rows: Option<Box<InvoiceRowFieldsToQuery>>, pub company: Option<Box<CompanyFieldsToQuery>>, pub customer: Option<Box<CustomerFieldsToQuery>>, pub payments: Option<Box<PaymentFieldsToQuery>>, } // 如果查询了payments且包含payment_method,返回已加载状态的Invoice pub fn query_invoice(fields: InvoiceFieldsToQuery) -> Invoice<Loaded> { // 实际数据库查询逻辑:根据fields加载对应字段并构建实例 todo!() } // 如果未查询payments,返回未加载状态的Invoice pub fn query_invoice_minimal(fields: InvoiceFieldsToQuery) -> Invoice<Unloaded> { todo!() }
步骤4:限制方法仅接收已加载状态的实例
修改payments方法,限定其参数必须是Invoice<Loaded>,这样编译器会直接阻止传入未加载对应字段的实例:
use std::fmt::Write; fn payments(invoice: &Invoice<Loaded>) -> String { let mut output = String::new(); for payment in &invoice.payments { let _ = write!( output, "mode: {mode} - amount: {amount}\n", mode = &payment.payment_method.mode, amount = payment.amount, ); } output } // 以下代码会编译失败:传入未加载状态的Invoice // fn bad_example() { // let invoice = query_invoice_minimal(InvoiceFieldsToQuery::default()); // payments(&invoice); // 编译错误:类型不匹配 // }
进阶:多字段组合状态控制
如果需要同时标记多个字段的状态,可以用元组组合标记类型,实现更精细的权限控制:
pub struct PaymentsLoaded; pub struct CustomerLoaded; pub struct Invoice<State> { pub id: Uuid, pub amount: i64, pub payments: <State as HasPayments>::Type, pub customer: <State as HasCustomer>::Type, // 其他字段 } pub trait HasPayments { type Type; } impl HasPayments for () { type Type = Option<Vec<Payment<Unloaded>>>; } impl HasPayments for PaymentsLoaded { type Type = Vec<Payment<Loaded>>; } impl<S> HasPayments for (PaymentsLoaded, S) { type Type = Vec<Payment<Loaded>>; } // 同理定义HasCustomer trait处理customer字段状态
内容的提问来源于stack exchange,提问作者Fred Hors
相关产品推荐
相关产品推荐

