2个不稳定版本
新版本 0.2.0 | 2024年8月11日 |
---|---|
0.1.0 | 2024年8月10日 |
#178 in 数据结构
每月215次下载
160KB
4.5K SLoC
一毛钱思考(关于Rust特性和自定义操作符)
又称:doubloon
这个库实现了一个 Money
数据类型,它同时支持静态和动态类型的 Currency
。也就是说,您可以创建一个 Money<USD>
,它与一个 Money<JPY>
完全不同,或者您可以创建一个 Money<&dyn Currency>
,其中货币是在运行时确定的,但仍可以安全地进行数学运算(例如,Money<&dyn Currency> + Money<&dyn Currency>
返回一个可能失败的 Result
,因为货币可能不同)。
我构建这个库的主要动机是更多地了解Rust特性和自定义操作符。但我最近也在寻找一个表示货币金额的crate,我注意到最流行的rusty_money已经有一段时间没有更新了,并且有多个已经超过一年的未解决的问题和pull请求。它的API和一系列行为相当不灵活:例如,它需要使用显式生命周期(这会自然地影响所有使用它的类型),并且当您在具有不同货币的实例上进行数学运算时,它会直接崩溃。
尽管我对Rust还不够熟悉,但我认为这种强大的语言特性可以支持更好的灵活体验,所以我构建了一些新的东西,并在过程中学到了很多关于Rust的知识!
要求
我希望Money数据类型具备以下功能
- 以高精度小数追踪金额:标准浮点数据类型不能用于货币金额,因为即使是简单的加法也可能产生相当奇怪的结果。常见的替代方案是追踪货币的较小单位(例如,美元的分),但当货币决定更改其较小单位数量时,这会变得很尴尬,就像冰岛在2007年所做的那样。这也使得表示分数较小单位变得困难,例如以八分之一美分表示的股价。
- 支持静态类型货币的实例:在某些应用程序中,您在编译时就知道货币类型,并希望确保一个货币单位的金额(如
Money
)不会意外地传递给期望不同货币金额的函数。换句话说,您希望Money<USD>
和Money<JPY>
是两个完全不同的类型,这样混合它们就会导致编译错误。 - 支持动态类型货币的实例:在其他应用程序中,您直到运行时才知道货币类型,因此我们也需要支持这一点。例如,您可能收到一个包含金额和三位字符货币代码的API请求,因此您需要在映射中查找货币并创建一个
Money<&dyn Currency>
。 - 允许进行等式比较:无论货币是静态还是动态类型,您都应该能够测试两个实例是否相等,因为这永远不会失败——它们可能不相等,但比较始终是合法的操作。
- 以安全方式支持数学运算:如果您添加两个
Money<USD>
实例,您应该得到一个Money<USD>
,因为编译器确保了货币相同。但是如果您添加两个Money<&dyn Currency>
实例,或者静态和动态类型货币的混合,您应该得到一个Result
,因为如果货币实际上不同,该操作可能会失败。类型Result
支持通过.and_then()
方法进行链式操作,因此仍然可以安全地处理多个项。
令人惊讶的是,Rust语言的功能确实使得所有这些成为可能!在本README的其余部分,我将解释我是如何实现这些功能的,并讨论一些尝试过的但不太奏效的方法。
货币特性和实现
第一步是定义一个所有货币都必须实现的Currency
特性。目前我将其保持简单,但将来可以扩展它以包括其他详细信息。
/// Common trait for all currencies.
pub trait Currency {
/// Returns the unique ISO alphabetic code for this currency
/// (e.g., "USD" or "JPY").
fn code(&self) -> &'static str;
/// Returns the number of minor units supported by the currency.
/// Currencies like USD and EUR currently support 2, but others
/// like JPY or KRW support zero.
fn minor_units(&self) -> u32;
/// Returns the symbol used to represent this currency.
/// For example `$` for USD or `¥` for JPY. Some currencies
/// use a series of letters instead of a special symbol
/// (e.g., `CHF` or `Lek`). If the currency has no defined
/// symbol, this will return an empty string.
fn symbol(&self) -> &'static str;
/// Returns the informal name for this currency.
fn name(&self) -> &'static str;
/// Returns the unique ISO numeric code for this currency.
fn numeric_code(&self) -> u32;
}
最初,我没有将这些方法作为参数包含 &self
,因为我认为这些方法的实现只会返回静态数据,但这在尝试构建一个动态类型货币的引用时产生了问题:&dyn Currency
。为此,Rust 要求特质必须是“对象安全”的,这意味着编译器可以构建 v-table 并执行动态调度。如果没有对 &self
的引用,就不知道在运行时调用特质的哪个实现,因此即使你从未在实现中引用它,&self
也必须是一个参数。
对于 Currency
的实例,我最初倾向于声明一个具有代码作为变体名称的 enum
,因为这是唯一的。但在 Rust 中,enum
是一种类型,并且该枚举的变体都是 同一类型的实例。所以如果我声明货币为类似 enum CurrencySet
的东西,所有的货币实例最终都会成为 Money<CurrencySet>
,这将违背我们支持静态类型货币的愿望。如果我只声明一个 CurrencyImpl
结构体并为各种货币声明其常量实例,也会出现同样的问题。
相反,我们需要每个 Currency
实现都成为它自己的 类型。最简单的方法是将它们声明为独立的 struct
,每个都 impl Currency
/// US Dollar
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct USD;
impl Currency for USD {
fn code(&self) -> &'static str {
"USD"
}
fn symbol(&self) -> &'static str {
"$"
}
fn name(&self) -> &'static str {
"US Dollar"
}
fn minor_units(&self) -> u32 {
2
}
fn numeric_code(&self) -> u32 {
840
}
}
/// Yen
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct JPY;
impl Currency for JPY {
fn code(&self) -> &'static str {
"JPY"
}
fn symbol(&self) -> &'static str {
"¥"
}
fn name(&self) -> &'static str {
"Yen"
}
fn minor_units(&self) -> u32 {
0
}
fn numeric_code(&self) -> u32 {
392
}
}
将 USD
和 JPY
声明为独立的 struct
使它们成为独立的 类型,这将使我们能够创建静态类型的 Money<USD>
与 Money<JPY>
。
货币类型
现在我们定义了一些货币,我们可以构建我们的 Money
类型
use rust_decimal::Decimal;
/// An amount of money in a particular currency.
#[derive(Debug, Clone)]
pub struct Money<C> {
amount: Decimal,
currency: C,
}
我们为货币定义了一个泛型类型参数 C
,但请注意,在结构体定义中我没有添加特征界限。也就是说,我只声明了 Money<C>
而不是 Money<C: Currency>
。当我最初开始学习Rust时,我倾向于在我的结构体定义上添加特征界限,但后来意识到这是不必要的,也是限制性的。因为当引用特征方法时,你必须在 impl
块中添加特征界限,并且它们唯一的创建或交互类型的方式是通过在 impl
块中定义的方法,因此通常不需要在结构体本身上添加特征界限。但这也过于限制性:我们不希望将 C
限制为只是一个 Currency
,因为我们还想支持一个 &dyn Currency
或甚至是一个 Box<dyn Currency>
。我们可以通过为 C
使用不同特征界限和类型的单独的 impl
块来实现这一点。
最初我尝试构造一个具有允许所有权的 Currency
实现或动态 Currency
引用的特征界限的单个 impl 块,但这实际上没有意义,因为 &dyn Currency
实际上是一个 类型 而不是 特征,因此不能用作特征界限。但它可以用作单独的 impl
块中泛型类型参数的类型,你将在下面看到。
我还考虑过为 &dyn Currency
实现 Currency
,这在Rust中是可能的,但这样会抹去两者之间的区别:这样就可以在具有 C: Currency
特征界限的 impl
块中使用 Money<&dyn Currency>
,代码实际上无法区分静态和动态类型的 Currency
。
因此,我开始使用一个没有特征界限的 impl
块,其中包含不关心 C
类型实际是什么的方法。
/// Common functions for statically and dynamically-typed currencies.
impl<C> Money<C> {
/// Constructs a new Money given a decimal amount and Currency.
/// The currency argument can be either an owned statically-typed
/// Currency instance, or a dynamically-typed reference
/// to a Currency instance (i.e., `&dyn Currency`).
pub fn new(amount: Decimal, currency: C) -> Self {
Self { amount, currency }
}
/// Returns a copy of the amount as a Decimal.
pub fn amount(&self) -> Decimal {
self.amount
}
}
new()
和amount()
方法实际上不需要知道类型C
究竟是什么,因此我们可以一次性定义它们。然而,这确实有一个有趣的缺点:可以为currency
参数传递任何类型,因此可以构造一个Money<String>
或Money<Foo>
,其中Foo
不是Currency
。虽然这听起来很奇怪,但可能没问题,因为如果不调用在另一个impl
块中定义的方法,你无法对那个Money
实例做很多事情,这些方法将为C
的类型设置限制。但如果这让你感到不悦,请参阅下面的“新标记特质”部分,那里有一个有趣的解决方案。
静态类型货币
下一个impl
块定义了特定于拥有静态类型Currency
实例的方法
/// Functions specifically for owned statically-typed Currency instances.
impl<C> Money<C>
where
C: Currency + Copy, // owned Currency instances can be Copy
{
/// Returns a copy of the Money's Currency.
pub fn currency(&self) -> C {
self.currency
}
}
在这里,我们在C
上添加了一个Currency + Copy
的特质约束,这意味着调用者使用的C
必须是一个支持复制语法的拥有Currency
实例。这使得我们可以从currency()
方法返回一个Currency
实例的副本。由于USD
和JPY
是单元结构体,复制它们不需要做太多工作,因此直接返回副本既方便又合适。
现在我们可以使用静态类型的Currency
创建Money
实例
// m_usd is type Money<USD>
let m_usd = Money::new(Decimal::ONE, USD);
assert_eq!(m_usd.currency(), USD);
assert_eq!(m_usd.amount(), Decimal::ONE);
// m_jpy is type Money<JPY>
let m_jpy = Money::new(Decimal::ONE, JPY);
assert_eq!(m_jpy.currency(), JPY);
assert_eq!(m_jpy.amount(), Decimal::ONE);
// This won't even compile because they are totally different types
// assert_eq!(m_usd, m_jpy);
动态类型货币
为了支持动态类型货币的引用,我们可以在另一个impl
块中提供泛型C
类型参数的具体类型,以支持动态类型货币
/// Functions specifically for borrowed dynamically-typed currencies.
impl<'c> Money<&'c dyn Currency> {
/// Returns the reference to the dynamically-typed Currency.
pub fn currency(&self) -> &'c dyn Currency {
self.currency
}
}
这里有几个需要注意的微妙之处。首先,我们不能像上面那样使用特质约束,因为&'c dyn Currency
是一个类型而不是一个特质。但这是可以的,因为我们可以在这个impl
块中将它作为C
的显式类型来使用。
其次,我们为impl
块声明了一个生命周期参数'c
,并将其用作Currency
引用的生命周期。这将使编译器强制要求Currency
实例至少与Money
实例一样长,这是好的,因为我们持有它的引用。幸运的是,调用者不需要在他们的代码中处理这个生命周期参数,因为编译器可以从上下文中推断出它。可以这样简单地做:
// CURRENCIES is a HashMap<'static str, &'static dyn Currency>
// so dynamic_currency is of type `&dyn Currency`
let dynamic_currency = CURRENCIES.get("USD").unwrap();
// money is of type `Money<&dyn Currency>`
let money = Money::new(Decimal::ONE, dynamic_currency);
assert_eq!(money.currency().code(), "USD");
let other_money = Money::new(Decimal::ONE, CURRENCIES.get("JPY").unwrap());
assert_eq!(other_money.currency().code(), "JPY");
第三,你可能惊讶地发现,我们可以在之前的 impl
块中声明另一个与刚刚声明的同名方法。Rust 允许这种情况,因为这些方法接受 &self
作为参数,因为它可以使用这个参数来确定正确的实现。在这种情况下,我们可以重新定义返回类型,使其为相同的引用,而不是一个静态类型 Currency
值的副本。
这里的意思是,不接受 &self
作为参数的方法不能被“重载”(换句话说)。例如,我最初尝试在不同的 impl
块中定义 new()
方法的不同版本,第一个接受一个拥有 Currency
值,第二个接受一个 &dyn Currency
引用,但 Rust 目前不允许这样做:声明将正常工作,但当你尝试使用这些 new()
方法之一时,你会得到一个错误,表明有多个候选者,它无法确定你想要调用哪一个。可能有一个语法来消除歧义,但我找不到它,这意味着我的调用者可能也找不到。
支持安全的货币数学
现在我们可以使用静态或动态类型的货币创建 Money
实例,因此让我们以安全的方式实现它们的加法。
Money<USD> + Money<USD>
应该返回Money<USD>
,因为这几乎是不可错的(尽管它仍然可能溢出)。Money<USD> + Money<JPY>
甚至不能编译。Money<&dyn Currency> + Money<&dyn Currency>
应该返回一个Result
,因为货币可能不同。Money<USD> + Money<&dyn Currency>
和Money<&dyn Currency> + Money<USD>
也应该是可能的,返回一个Result
,其Ok
类型为左侧的类型。
令人惊讶的是,Rust 使得所有这些成为可能。Add
特性不仅允许你为右侧项指定不同的类型,而且还可以为操作的 Output
指定不同的类型!
静态类型的实现相当直接
/// Adds two Money instances with the same statically-typed currencies.
/// Attempting to add two instances with _different_ statically-typed
/// Currencies simply won't compile.
impl<C> Add for Money<C>
where
C: Currency,
{
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self {
amount: self.amount + rhs.amount,
currency: self.currency,
}
}
}
对于动态类型的版本,我们定义一个 MoneyMathError
枚举,并将 Output
关联类型设置为 Result<Self, MoneyMathError>
/// Errors that can occur when doing math with Money instances that
/// have dynamically-typed currencies
#[derive(Debug, Error, PartialEq, Clone)]
pub enum MoneyMathError {
#[error("the money instances have incompatible currencies ({0}, {1})")]
IncompatibleCurrencies(&'static str, &'static str),
}
/// Adds two Money instances with dynamically-typed currencies.
/// The Output is a Result instead of a Money since the operation
/// can fail if the currencies are incompatible.
impl<'c> Add for Money<&'c dyn Currency> {
type Output = Result<Self, MoneyMathError>;
fn add(self, rhs: Self) -> Self::Output {
if self.currency.code() == rhs.currency.code() {
Ok(Self {
amount: self.amount + rhs.amount,
currency: self.currency,
})
} else {
Err(MoneyMathError::IncompatibleCurrencies(
self.currency.code(),
rhs.currency.code(),
))
}
}
}
我们再次将 &'c 动态货币
规定为泛型类型参数的显式类型,因为它是一个类型,而不是一个特质,所以不能将其表示为特质边界。我们还检查货币是否相同,如果不同则返回错误。
通过指定 Add
特质中的右侧类型(默认为 Self
),也可以支持静态和动态货币的混合。
/// Adds a Money instance with a statically-typed Currency to
/// a Money instance with a dynamically-typed Currency. The output
/// is a Result since the operation can fail if the currencies are
/// incompatible.
impl<'c, C> Add<Money<&'c dyn Currency>> for Money<C>
where
C: Currency,
{
type Output = Result<Self, MoneyMathError>;
fn add(self, rhs: Money<&'c dyn Currency>) -> Self::Output {
if self.currency.code() == rhs.currency.code() {
Ok(Self {
amount: self.amount + rhs.amount,
currency: self.currency,
})
} else {
Err(MoneyMathError::IncompatibleCurrencies(
self.currency.code(),
rhs.currency.code(),
))
}
}
}
/// Adds a Money instance with a dynamically-typed Currency to
/// a Money instance with a statically-typed Currency. The Output
/// is a Result since the operation can fail if the currencies are
/// incompatible.
impl<'c, C> Add<Money<C>> for Money<&'c dyn Currency>
where
C: Currency,
{
type Output = Result<Self, MoneyMathError>;
fn add(self, rhs: Money<C>) -> Self::Output {
if self.currency.code() == rhs.currency.code() {
Ok(Self {
amount: self.amount + rhs.amount,
currency: self.currency,
})
} else {
Err(MoneyMathError::IncompatibleCurrencies(
self.currency.code(),
rhs.currency.code(),
))
}
}
}
有了这些,现在我们可以这样做货币数学
// statically-typed
assert_eq!(
Money::new(Decimal::ONE, USD) + Money::new(Decimal::ONE, USD),
Money::new(Decimal::TWO, USD),
);
// dynamically-typed, same currency -> Ok
let currency_usd = CURRENCIES.get("USD").unwrap();
assert_eq!(
Money::new(Decimal::ONE, currency_usd) + Money::new(Decimal::ONE, currency_usd),
Ok(Money::new(Decimal::TWO, currency_usd)),
);
// dynamically-typed, different currencies -> Err
let currency_jpy = CURRENCIES.get("JPY").unwrap();
assert_eq!(
Money::new(Decimal::ONE, currency_usd) + Money::new(Decimal::ONE, currency_jpy),
Err(MoneyMathError::IncompatibleCurrencies(
currency_usd.code(),
currency_jpy.code(),
)),
);
// dynamically-typed + statically-typed, same currency -> Ok(dynamically-typed)
assert_eq!(
Money::new(Decimal::ONE, currency_usd) + Money::new(Decimal::ONE, USD),
Ok(Money::new(Decimal::TWO, currency_usd)),
);
// dynamically-typed + statically-typed, different currencies -> Err
assert_eq!(
Money::new(Decimal::ONE, currency_usd) + Money::new(Decimal::ONE, JPY),
Err(MoneyMathError::IncompatibleCurrencies(
currency_usd.code(),
JPY.code()
)),
);
// statically-typed, multi-term
assert_eq!(
Money::new(Decimal::ONE, USD)
+ Money::new(Decimal::ONE, USD)
+ Money::new(Decimal::ONE, USD),
Money::new(Decimal::new(3, 0), USD),
);
// dynamically-typed, multi-term using Result::and_then()
// (if an error occurs, closures are skipped and final result is an error)
assert_eq!(
(Money::new(Decimal::ONE, currency_usd) + Money::new(Decimal::ONE, currency_usd))
.and_then(|m| m + Money::new(Decimal::ONE, currency_usd)),
Ok(Money::new(Decimal::new(3, 0), currency_usd)),
);
在实际代码中,定义了用于二进制和一元运算的宏,这使得 Money
也支持减法、乘法、除法、取余和负号运算,使用相同的技巧。
相等比较
上面的断言依赖于比较 Money
实例是否相等的能力,这需要为静态和动态货币都实现 PartialEq
特质。
/// Allows equality comparisons between Money instances with statically-typed
/// currencies. The compiler will already ensure that `C` is the same for
/// both instances, so only the amounts must match.
impl<C> PartialEq for Money<C>
where
C: Currency + PartialEq,
{
fn eq(&self, other: &Self) -> bool {
self.amount == other.amount && self.currency == other.currency
}
}
/// Allows equality comparisons between Money instances with dynamically-typed
/// currencies. Both the amount and the currency codes must be the same.
impl<'c> PartialEq for Money<&'c dyn Currency> {
fn eq(&self, other: &Self) -> bool {
self.amount == other.amount && self.currency.code() == other.currency.code()
}
}
与数学运算类似,我们还可以通过在 PartialEq
特质中指定右侧类型(默认为 Self
)来支持静态和动态货币的混合比较。
/// Allows equality comparisons between Money instances with dynamically-typed
/// currencies and those with statically-typed currencies
impl<'c, C> PartialEq<Money<&'c dyn Currency>> for Money<C>
where
C: Currency,
{
fn eq(&self, other: &Money<&'c dyn Currency>) -> bool {
self.amount == other.amount && self.currency.code() == other.currency.code()
}
}
/// Allows equality comparisons between Money instances with dynamically-typed
/// currencies and those with statically-typed currencies
impl<'c, C> PartialEq<Money<C>> for Money<&'c dyn Currency>
where
C: Currency,
{
fn eq(&self, other: &Money<C>) -> bool {
self.amount == other.amount && self.currency.code() == other.currency.code()
}
}
支持 PartialOrd
的技术(更多或更少)是相同的。这个特质允许你在两个实例不可比时返回 None
,当动态类型货币不同时,我们就是这样返回的。
格式化
这个包还支持将 Money
实例格式化为字符串以进行显示。格式化器让你完全控制 Money
实例的外观。
let m = Money::new(Decimal::new(123456789,2), EUR);
assert_eq!(m.format(&Formatter::default()), Ok("€1,234,567.89".to_string()));
let custom_formatter = Formatter {
decimal_separator = ",",
digit_group_separator = ".",
positive_template: "{a} {s}",
negative_template: "({a} {s})",
..Default::default()
};
assert_eq!(m.format(&custom_formatter), Ok("1.234.567,89 €".to_string()));
当构建自定义的 Formatter
时,你可以为正数和负数指定格式化模板。格式化的金额永远不会包含正/负号,即使金额是负的,这样你就可以控制符号在相应模板中的位置。或者,你可以使用负数的会计表示法,其中它被括号括起来。
格式化模板可以使用以下任何替换令牌
{a}
= 根据其他属性格式化的金额(例如,“1,000.00”)。这永远不会包含正/负号,即使金额是负的,这样你就可以使用模板来控制符号的位置。{s}
= 货币符号(例如,“$”),如果没有符号则为空。{c}
= 货币代码(例如,“USD”)。{s|c}
= 货币符号,如果没有符号则为货币代码。{s|c_}
= 与{s|c}
相同,但当没有符号时,代码包括一个尾随空格,以便在它出现在金额之前时与金额偏移。{s|_c}
与{s|c}
相同,但其中没有符号,代码包含一个前导空格,以使其在金额之后出现时与金额隔开。
在您想要货币符号与格式化金额相邻时,最后两个选项非常有用,但回退到代码时,您希望在其前后留出空格,以使其与格式化金额隔开。例如
// XAU = Gold, which has no symbol, and 0 minor units
let m = Money::new(Decimal:new(12345,0), XAU);
// The default format uses `{s|c_}`, so when falling back
// to the code, it adds a space between the code and amount.
assert_eq!(m.format(&Formatter::default()), Ok("XAU 12,345".to_string()));
默认情况下,金额将被四舍五入并格式化为货币的较小单位数。但您可以通过在 Formatter
上设置 decimal_places
属性来覆盖此设置。
// XAU = Gold, which has no symbol, and 0 minor units
let m = Money::new(Decimal:new(12345,0), XAU);
let f = Formatter {
decimal_places: Some(2),
..Default::default()
};
assert_eq!(m.format(&f), Ok("XAU 12,345.00".to_string()));
Formatter
还支持不规则的数字分组,例如印度使用的 lakh 和 crore 系统。
let m = Money::new(Decimal:new(123456789,0), INR);
let f = Formatter {
// these are expressed right-to-left, so this pattern
// causes the three right-most digits to be grouped,
// then the next 2 digits to the left of those, and then
// the next 2 digits to the left of those, and then the
// rest without any grouping.
digit_groupings: Some(&[3,2,2]),
..Default::default()
};
assert_eq!(m.format(&f), Ok("₹12,34,56,789.00".to_string()));
默认情况下,零值使用 positive_template
格式化,但您可以为零值指定不同的模板。
let m = Money::new(Decimal::ZERO, USD);
let f = Formatter {
zero_template: Some("free!"),
..Default::default()
};
assert_eq!(m.format(&f), Ok("free!".to_string()));
Serde
该库还支持通过可选的 serde
功能通过 serde 进行序列化。
cargo add doubloon --features serde
在序列化一个 Money
实例时,它将写入一个包含两个字段的架构:金额作为字符串,货币代码作为字符串。例如,将 Money::new(Decimal::ONE, USD)
序列化为 JSON 得到以下结果
{
"amount": "1",
"code": "USD"
}
不幸的是,serde 反序列化不支持任何类型的调用者提供的上下文,因此该库没有通用的方法将序列化的货币代码转换回适当的 &dyn Currency
。由于调用者可能实现自己的 Currency
实例以支持特定应用程序的货币,因此该库无法使用单个众所周知的全局映射来解析货币代码。
为了支持反序列化,您的应用程序应将反序列化到这样的架构中
#[derive(Debug, Deserialize)]
pub struct DeserializedMoney {
pub amount: Decimal,
pub code: String,
}
然后,您可以将 code
解析为适当的 &dyn Currency
并使用该架构构建一个 Money
实例。
新标记特征
当我们第一次看到 Money::new()
方法时,我指出它实际上允许人们使用不是 Currency
的东西来构造一个 Money
。最初我试图通过将 new()
放入特定的 impl
块中来解决此问题,但这无法编译
// DOES NOT COMPILE!
impl<C> Money<C>
where
C: Currency,
{
pub fn new(amount: Decimal, currency: C) -> Self {
Self { amount, currency }
}
}
impl<'c> Money<&'c dyn Currency>
{
pub fn new(amount: Decimal, currency: &'c dyn Currency) -> Self {
Self { amount, currency }
}
}
fn main() {
// COMPILE ERROR: multiple candidates
let m_static = Money::new(Decimal::ONE, USD);
let m_dynamic = Money::new(Decimal::ONE, &USD as &dyn Currency);
}
我不确定为什么编译器无法确定要调用哪个版本的 new()
,给定参数类型不同,但现在它不起作用。
尽管我们无法构造一个允许拥有 Currency
的实现或动态引用的单一特征约束,但我们可以定义一个新的特征并为这两件事情做空白实现。例如
// New marker trait, with blanket implementations for anything that
// implements Currency, and any `&'c dyn Currency`
pub trait CurrencyOrRef {}
impl<C> CurrencyOrRef for C where C: Currency {}
impl<'c> CurrencyOrRef for &'c dyn Currency {}
// Single impl block using CurrencyOrRef as trait bound
impl<C> Money<C>
where
C: CurrencyOrRef,
{
pub fn new(amount: Decimal, currency: C) -> Self {
Self { amount, currency }
}
}
fn main() {
// Now this compiles
let m_static = Money::new(Decimal::ONE, USD);
let m_dynamic = Money::new(Decimal::ONE, &USD as &dyn Currency);
}
现在无法构造一个 Money<String>
或 Money<Foo>
,其中 Foo
不是一个 Currency
。但是,调用者在自己的 Foo
类型上实现 CurrencyOrRef
标记特质的做法也不算不可能,所以我认为这最终是否真的值得还不太清楚。
但这种技术确实使得支持可能需要 Currency
特质子集的其他构造函数变得更加容易。例如,如果我们想支持从一定数量的货币辅币单位创建一个 Money
,我们需要知道货币支持多少辅币单位,这是 Currency
特质中的一个方法。我们可以通过使这里的标记特质更加智能来实现这一点
/// Used as a trait bound when constructing new instances of Money
/// from minor units.
pub trait MinorUnits {
fn minor_units(&self) -> u32;
}
/// Blanket implementation for any static [Currency] instance.
impl<C> MinorUnits for C
where
C: Currency,
{
fn minor_units(&self) -> u32 {
self.minor_units()
}
}
/// Implementation for an `&dyn Currency`.
impl<'c> MinorUnits for &'c dyn Currency {
fn minor_units(&self) -> u32 {
(*self).minor_units()
}
}
/// Methods that require knowing the `minor_units` of the currency.
impl<C> Money<C>
where
C: MinorUnits,
{
/// Construct a Money from a decimal amount and currency.
/// (This doesn't strictly need the minor units but we include
/// it here to take advantage of the marker trait).
pub fn new(amount: Decimal, currency: C) -> Self {
Self { amount, currency }
}
/// Constructs a Money from some number of minor units in the
/// specified Currency. For example, 100 USD minor units is one USD,
/// but 100 JPY minor units is 100 JPY.
pub fn from_minor_units(minor_units: i64, currency: C) -> Self {
Self {
amount: Decimal::new(minor_units, currency.minor_units()),
currency,
}
}
}
这使得标记特质更有用,也许值得这么做。
待办事项
我还需要完成以下内容
- 辅助方法:可能需要添加各种辅助方法,例如
split()
,用于对辅币单位有意识的拆分(例如,剩余的便士被分配到桶的子集中)。
更正或建议?
是否有更好的方法来做这件事?我比较新接触 Rust,可能有一些我没有遇到过的机制可以提供更好的解决方案。如果你知道什么,请打开一个问题并告诉我!我将相应地更新代码和 README。
依赖关系
~1–1.6MB
~34K SLoC