After writing my 10,000th check to ensure a decimal argument is greater than or equal to zero, I wondered:
Wouldn’t it be easier to create a custom decimal struct that enforces this rule, much like uint
?
After looking at the Decimal.cs
source, I’m having second guesses.
There’s a lot going on in there, and it’s not hard to imagine baking this into an application – and then finding some critical problem.
Are there any libraries out there that have already done this?
Or, are there reasons this is a terrible idea?
2
Well, with a help of generic math you can try implementing UDecimal
based on Decimal
if you like:
using System.Numerics;
...
public struct UDecimal
: IAdditionOperators<UDecimal, UDecimal, UDecimal>, // We can add UDecimals
ISubtractionOperators<UDecimal, UDecimal, UDecimal> // We can subtract UDecimals etc.
{
private readonly decimal _value;
public UDecimal(decimal value)
{
ArgumentOutOfRangeException.ThrowIfNegative(value);
_value = value;
}
public static UDecimal operator +(UDecimal left, UDecimal right) =>
new UDecimal(left._value + right._value);
public static UDecimal operator -(UDecimal left, UDecimal right) =>
new UDecimal(left._value - right._value);
...
}
There are many interfaces you should implement, but almost all the methods are evident oneliners.
However, I suggest implementing your custom class, say
public struct Price {
public decimal Value {get;}
public Price(decimal value) {
// Note some extra validation
if (value < 0 || value > 1_000_000)
throw new ArgumentOutOfRangeException(nameof(value));
Value = value;
}
// Some extra property etc.
public decimal Vat => Value * 0.20M;
}
3
I would not try to implement a new numeric type but instead implement the logic on a higher level. Say, you have a bank account class. Then you can ensure that withdrawals don’t exceed the balance.
public class BankAccount
{
public BankAccount()
{
}
public BankAccount(decimal balance)
{
if (balance < 0) {
throw new ArgumentException("Initial balance must not be negative");
}
Balance = balance;
}
public decimal Balance { get; private set; }
public void Deposit(decimal amount)
{
if (amount <= 0) {
throw new ArgumentException("Amount must be positive");
}
Balance += amount;
}
public void Withdraw(decimal amount)
{
if (amount <= 0) {
throw new ArgumentException("Amount must be positive");
}
if (amount > Balance) {
throw new InvalidOperationException("Balance too small for this withdrawal.");
}
Balance -= amount;
}
}
1