trait方法的返回类型如何设计?

问题描述 投票:0回答:1

我开始为 SKU 实现一个实用程序,即一个产品需要多个单位。基本逻辑是:

  • 将金额列表转换为具有基本单位的单个金额。
  • 将带有基本单位的单个金额拆分为人类可读的金额列表。

示例

let five = "5kg".parse::<Amount>()?;
let two = "2g".parse::<Amount>()?;

let sum = five + two;
let result = sum * 3;

let result = Weight.reduce(result)?;
assert_eq!(result, Amount::new(15006, Weight.base_unit()));

let result = Weight.split(result)?.into_iter().collect::<Vec<_>>();
assert_eq!(result, [Amount::new(15, kg()), Amount::new(6, g())]);

设计

代码设计如下:

use std::ops::{Add, Mul};

trait Exchanger {
    type Err;

    fn rate(&self, unit: &Unit) -> Result<u32, Self::Err>;

    fn reduce<E>(&self, expr: E) -> Result<Amount, Self::Err>
    where
        for<'a> E: Reduce<&'a Self>,
        Self: Sized,
    {
        expr.reduce(self)
    }

    fn split<E>(&self, expr: E) -> Result<Split, Self::Err>
    where
        for<'a> E: Reduce<&'a Self>,
        Self: Sized,
    {
        let base = expr.reduce(self)?;
        todo!()
    }
}

impl<T: Exchanger> Exchanger for &T {
    type Err = T::Err;

    fn rate(&self, unit: &Unit) -> Result<u32, Self::Err> {
        (**self).rate(unit)
    }
}

trait Reduce<E: Exchanger> {
    fn reduce(self, e: E) -> Result<Amount, E::Err>;
}

trait Expression<E: Exchanger, Rhs, T>: Reduce<E> + Add<Rhs> + Mul<T> {}

// impls Expression<E, Rhs, T>
struct Amount(u32, Unit);

struct Unit(String);

// impls Expression<E, Rhs, T>
struct Sum<L, R>(L, R);

// impls Expression<E, Rhs, T>
struct Split(Vec<Amount>);

问题

我觉得上面的设计有问题,不够优雅。

  1. 第一个是我需要的 定义一个额外的结构体

    Split
    ,因为
    Exchanger::split
    的返回类型可能是:
    Amount
    Sum
    ,但这在中很好,只需引入像
    Expression
    这样的接口。我尝试使用
    Box<dyn Expression>
    但它需要通用参数,我无法在编译时提供它们。

  2. 我需要为新类型

    Expression
    重新实现
    Split

  3. 每当我添加实现

    T
    的新类型
    Expression
    时,我还必须为所有已实现
    Add<T>
    的类型实现
    Expression
    , 这样的组合是爆炸性的,例如:

    struct NewType;
    impl Add<NewType> for NewType {}
    impl Add<Amount> for NewType {}
    impl Add<Sum> for NewType {}
    impl Add<Split> for NewType {}
    
    impl Add<NewType> for Amount {}
    impl Add<NewType> for Sum {}
    impl Add<NewType> for Split {}
    
    // impl all reference types, and so on.
    impl Add<NewType> for &Amount {}
    
  4. 我还需要实现引用类型的所有特征,例如:

    trait Trait {}
    
    impl <T: Trait> Trait for &T{}
    

你能告诉我更好的方法来实现当前的设计吗? 您还可以参考github存储库查看完整代码。

rust types traits
1个回答
0
投票

如果您只需要执行您所描述的基本逻辑,我会做这样的事情:

use std::{error::Error, fmt::Display, num::ParseIntError, ops::Add, str::FromStr};

/// A type representing all units of a certain quantity (e.g kg, g, t, etc.)
pub trait Unit: Sized {
    /// The base unit. All others units are a multiples of it.
    fn base() -> Self;

    /// The magnitude of this unit with respect to the base unit. The base unit itself hasµ
    /// magnitude 1.
    ///
    /// For example, if the base unit is the gram, then the kilogram would have a magnitude of
    /// 1000.
    fn magnitude(&self) -> u32;

    /// All units of this quantity, in decreasing order of magnitude.
    ///
    /// For example: [t, kg, g]
    fn sorted_units() -> Vec<Self>;
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Quantity<U> {
    pub amount: u32,
    pub unit: U,
}

impl<U> Quantity<U> {
    pub fn new(amount: u32, unit: U) -> Self {
        Self { amount, unit }
    }
}

impl<U: Unit> Quantity<U> {
    /// Splits this quantity into different units
    pub fn split(&self) -> Vec<Self> {
        let mut amount = self.amount * self.unit.magnitude();
        let mut split = vec![];
        for unit in U::sorted_units() {
            let mag = unit.magnitude();
            let whole = amount / mag;
            if whole != 0 {
                split.push(Self {
                    amount: whole,
                    unit,
                })
            }
            amount -= whole * mag;
        }
        split
    }
}

impl<U: Unit> Add for Quantity<U> {
    type Output = Self;
    /// Output always in terms of the "base unit"
    fn add(self, rhs: Self) -> Self::Output {
        Self {
            amount: self.amount * self.unit.magnitude() + rhs.amount * rhs.unit.magnitude(),
            unit: U::base(),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseQuantityError<UErr> {
    InvalidAmount(ParseIntError),
    InvalidUnit(UErr),
}

impl<U: FromStr> FromStr for Quantity<U> {
    type Err = ParseQuantityError<<U as FromStr>::Err>;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let first_non_digit_idx = s
            .char_indices()
            .find(|(_, c)| !c.is_ascii_digit())
            .map(|(idx, _)| idx)
            .unwrap_or(s.len());

        let amount = s[..first_non_digit_idx]
            .parse::<u32>()
            .map_err(ParseQuantityError::InvalidAmount)?;

        let unit = s[first_non_digit_idx..]
            .parse::<U>()
            .map_err(ParseQuantityError::InvalidUnit)?;

        Ok(Self { amount, unit })
    }
}

impl<UErr: Display> Display for ParseQuantityError<UErr> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::InvalidAmount(err) => write!(f, "failed to parse `Quantity` amount: {err}"),
            Self::InvalidUnit(err) => write!(f, "failed to parse `Quantity` unit: {err}"),
        }
    }
}

impl<UErr: Error> Error for ParseQuantityError<UErr> {}

这种设计有多重优点:

  • 很小
  • 每个维度都可以是一个单独的类型(很可能是枚举),这使得像添加公斤和升这样奇怪的操作变得不可能
  • 创建一个新单位只是实现
    Unit
    以及可能的标准
    FromStr
     特征

例如,用多个单位表示质量:

use std::{error::Error, fmt::Display, str::FromStr};

pub type Mass = Quantity<MassUnit>;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MassUnit {
    G,
    Kg,
}

impl Unit for MassUnit {
    fn base() -> Self {
        Self::G
    }

    fn magnitude(&self) -> u32 {
        match self {
            Self::G => 1,
            Self::Kg => 1000,
        }
    }

    fn sorted_units() -> Vec<Self> {
        vec![Self::Kg, Self::G]
    }
}

#[derive(Debug)]
pub struct InvalidMassUnit;

impl FromStr for MassUnit {
    type Err = InvalidMassUnit;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "g" => Ok(Self::G),
            "kg" => Ok(Self::Kg),
            _ => Err(InvalidMassUnit),
        }
    }
}

impl Display for InvalidMassUnit {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "invalid mass unit")
    }
}

impl Error for InvalidMassUnit {}

可以这样使用:

let five_kg = "5kg".parse::<Mass>()?;
let two_g = "2g".parse::<Mass>()?;

let sum = five_kg + two_g;
assert_eq!(sum, Mass::new(5002, MassUnit::base()));

let split = sum.split();
assert_eq!(
    split,
    [Mass::new(5, MassUnit::Kg), Mass::new(2, MassUnit::G)]
);

我冒昧地删除了

Sum
类型,但如果您认为需要它作为计算中的额外层,那么添加它应该不会太难。


我还假设您确实需要能够用不同的单位表示相似的数量;否则,问题几乎消失了,你可以用

newtype
替换每个 Quantity<U> 类型,如下所示:

pub struct Grams(u32);

pub struct Millimeters(u32);

您可能希望保留

Quantity<U>
作为
split
的返回值,如果它不能只存在于
Display
impl 中。

© www.soinside.com 2019 - 2024. All rights reserved.