You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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())
    ));

我的需求是:

  1. 避免使用expect,通过编译期检查确保字段已填充
  2. 理想情况下,当代码需要使用payments或payment_method时,编译期能强制要求必须通过query_payments指定查询该字段
  3. 字段必须保留按需加载的特性——只在需要时才查询,不需要时不加载

解决方案:利用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 17:24:51