C# 中的 BigDecimal






4.98/5 (20投票s)
本文介绍了一个类似于BigInteger的类,但它支持浮点值,如十进制数。
开始之前
正如你所见,表达式的优先级(包括括号)得到了尊重,并且如果我们需要的,我们可以要求100位小数(如果需要,还可以更多)。现在,我们来看正文。
引言
C#(以及.NET)目前不提供BigDecimal
类。它们只提供了BigInteger
类。
有趣的是,在我看来,BigInteger
和BigDecimal
之间唯一的真正区别在于,BigDecimal有一个小数点(在美国标准中是点),用于分隔整数和小数/浮点数。
所以,我想做的是:创建一个类,将任何数字存储为BigInteger
,并且还有一个字段(我也将其设置为BigInteger
类型)来告诉小数点确切的位置。
也许使用BigInteger
作为小数点位置有点过了,`int`或`long`就足够了,但我希望能覆盖尽可能大的范围。
类的工作原理
这就是乐趣开始的地方。在阅读网上许多文章时,我看到需要将事物分成3个或更多部分。我决定从简单开始:一个整数(可以非常大,所以我使用了BigInteger
),以及另一个数字来告诉小数点(小数点)的位置。
如果小数点位于末尾的第0
位,这意味着该数字是整数。如果小数点位于数字位数的确切位置,这意味着数字紧跟在"0."
之后 - 即,小数点位于末尾2
位的57
,变成0.57
。
事实上,在我最初的实现中,我不允许小数点超出整数的位数,但这不允许我生成像0.01
这样的数字。在这种情况下,整数是1
,但浮点位置是2
,这比数字实际拥有的位数还要多(现在已修复/允许)。
构造函数
该类有两个主要的构造函数。其中一个只接收一个BigInteger
。它假定没有浮点数(或者它位于末尾的第0位),并调用另一个构造函数。
然后,还有一个第二个构造函数,它接收整个数字作为BigInteger
,以及一个值,该值告诉小数点的位置,从最后一个位置开始计数。所以,数字是123
还是1000
并不重要……第二个参数为2
意味着小数点后将有两位数字。因此,分别是1.23
和10.00
……但是,任何左侧(小数点前)的零或右侧(小数点后)的零都将被消除,因此数字将分别变为1.23
和10
。
也许这并不真的必要,但我开始编写代码时使用的是无符号类型……所以,而不是存储正数和负数的BigInteger
,我总是将它们存储为正值,如果数字是负数,我只设置一个标志。
第一个运算符… operator +
我决定的第一个运算符是operator +
(加法)。
我的第一个想法是:我需要使数字兼容。
该类仅存储整数,但小数点的位置可能值不同。因此,我们看到的101
和1.01
都存储为101
,但其中一个小数点位于0
,而另一个小数点位于2
。
所以,我的解决方案是取小数点值最大的那个(在本例中为2),并将所有数字调整为使用该小数点。
这意味着小数点位于0
的101
将变成小数点位于2
的10100
,而小数点位于2
的101
将保持不变。
现在,只需将它们相加(10100 + 101 = 10201)
,并使用最高的小数点索引,将数字转换为102.01
。
整个代码看起来像这样
public static BigDecimal operator + (BigDecimal a, BigDecimal b)
{
var digitFromEnd0 = a._decimalIndexFromEnd;
var digitFromEnd1 = b._decimalIndexFromEnd;
var maxIndex = BigInteger.Max(digitFromEnd0, digitFromEnd1);
var intA = _MakeItHaveThisAmountOfFloatDigits(a, maxIndex);
var intB = _MakeItHaveThisAmountOfFloatDigits(b, maxIndex);
if (a._isNegative)
intA = -intA;
if (b._isNegative)
intB = -intB;
var sum = intA + intB;
return new BigDecimal(sum, maxIndex);
}
方法_MakeItHaveThisAmountOfFloatDigits
将在位数相同时返回输入数字,或者会在右侧添加一些零,直到达到正确的位数。
看起来是这样的:
private static BigInteger _MakeItHaveThisAmountOfFloatDigits
(
BigDecimal value,
BigInteger numberOfFloatDigits
)
{
if (value._decimalIndexFromEnd == numberOfFloatDigits)
return value._value; // Already right size. Do nothing.
var diff = numberOfFloatDigits - value._decimalIndexFromEnd;
var multiplier = new BigInteger(1);
for (var i=0; i<diff; i++)
multiplier *= 10;
var intPart = value._value * multiplier;
return intPart;
}
其他运算符
operator - (减法)
减法运算符与加法运算符非常相似。我首先使两个数字具有相同的位数……也就是说,小数点右侧位数最多的那个需要乘以正确的10的幂,然后我进行减法。
构造新数字时,由于构造函数会负责删除小数点右侧的任何尾随零,因此一切都会正常工作。
这是operator -
的全部代码:
public static BigDecimal operator - (BigDecimal a, BigDecimal b)
{
var digitFromEnd0 = a._decimalIndexFromEnd;
var digitFromEnd1 = b._decimalIndexFromEnd;
var maxIndex = BigInteger.Max(digitFromEnd0, digitFromEnd1);
var intA = _MakeItHaveThisAmountOfFloatDigits(a, maxIndex);
var intB = _MakeItHaveThisAmountOfFloatDigits(b, maxIndex);
if (a._isNegative)
intA = -intA;
if (b._isNegative)
intB = -intB;
var subtraction = intA - intB;
return new BigDecimal(subtraction, maxIndex);
}
乘法 (operator *)
我以为乘法会很难实现……但事实证明它比加法和减法更容易实现。
我只需将两个值的整数表示相乘,然后将它们的小数点“索引”(从末尾开始)值相加。然后用这个创建新值。就是这样。
代码如下:
public static BigDecimal operator * (BigDecimal a, BigDecimal b)
{
var digitFromEnd0 = a._decimalIndexFromEnd;
var digitFromEnd1 = b._decimalIndexFromEnd;
var intA = a._value;
var intB = b._value;
// It doesn't matter who is negative... as long as one is negative and the
// other is not, we must make the result negative.
bool isResultNegative = (a._isNegative != b._isNegative);
var sum = a._value * b._value;
if (isResultNegative)
sum = -sum;
var indexFromEnd = a._decimalIndexFromEnd + b._decimalIndexFromEnd;
return new BigDecimal(sum, indexFromEnd);
}
除法 (operator / 和 Divide 方法)
除法更复杂,我在这里不解释。默认情况下,operator /
将调用Divide
方法,并将小数点后的位数设置为8。
理论上,你可以传入任何你想要的数字,但我注意到,超过一千位后,代码会变得非常慢……所以,除非你真的、真的需要,否则尽量不要设置那么大的位数。
示例应用程序默认使用100位小数。它只是对两个数字进行除法(需要使用.作为小数点,你不能独立于你的计算机区域/设置使用不同的分隔符),你可以看到它支持.NET提供的decimal
无法处理的真正大的数字,例如4328947293473294729347329847329432
作为被除数和23423432432432432423423
作为除数会导致decimal
出错,但BigDecimal
可以轻松处理这些数字。
还有什么?
我实现了Power
方法。我仍然不确定是否应该将其命名为Pow
并使其成为static
,或者将其保留为Power
并作为实例方法。事实上,我倾向于将Divide
设为实例方法,而不是将其保留为static
方法。
无论如何,我编写了许多单元测试,但仍然发现了一些测试未能覆盖的错误(所以,我写了更多测试来证明问题……然后来证明修复)。
将代码视为仍在开发中,但特别是对于加法和减法,它应该效果相当好。
与BigInteger
不同,此类在调用ToString()
时不会限制其生成的字符串大小,因此,如果你使用非常大的数字,请注意,查看BigDecimal
中存储的值可能会减慢甚至崩溃你的调试器。
好了……就这些了。如果你愿意,请下载示例,看看它是否适合你。我希望它能对许多人有所帮助。并使用论坛在此报告你可能发现的任何错误。
一个额外的细节
我将此类设置为在0
除以0
时返回1
。这是为了与公式中的简化相兼容,其中x/x
总是为一。如果x
为0
且你不进行简化,0/0
在大多数情况下会抛出异常。在这个类中,它将完全匹配并返回1
。这不是一个错误。这是该类的一个特性。
结论?
你来告诉我。对我来说,它已经帮助我解决了在处理BigInteger
不适用的非常大的数字时遇到的十进制等问题。
BigRational
如果你下载了最新版本的示例,你可能会注意到还有一个BigRational
类。这个类的想法很简单,但实现可能不像它应该的那样清晰。
为了避免在不断进行乘法、除法、然后乘法、然后除法(依此类推)时产生精度错误,BigRational
类所做的是,在进行实际乘法时它会保持乘以分子,在除法时则乘以分母。它只会在我们要求将BigRational
的值转换为BigDecimal
时才执行实际的除法。
那是唯一可能会损失一些精度的时候,但假设我们在调用AsBigDecimal
之前可能已经进行了数百次乘法和除法,我们将避免数百次精度损失。
Add
和Subtract
方法可能有点令人困惑,因为它们实际上乘以了分母……这就是它们按预期工作的原因,因为只有在我们已经进行除法(或分母为1)的情况下,它们才能直接相加。
此外,为了避免数值无限增长,所有操作(Add
、Subtract
、Multiply
和Divide
)都会调用一个方法来“压缩”数字。例如,如果分子和分母都是偶数,我们可以将两者都除以2
。这意味着两个数字都变小了,但它们表示的比率保持不变。所以,如果你存储100
和50
但实际类存储的是2
和1
,请不要感到惊讶。
与BigDecimal
不同,这是一个类。我曾想让它成为一个结构体,但我需要分母从1开始,而C#会自然地允许创建一个“默认”结构体(带有零),而不是试图处理分母为0
的情况,所以我决定将BigRational
设为一个实际的类。如果我收到足够多的请求,我可能会改变这个决定。
好吧……希望你喜欢这两个类。
历史
- 2023年8月9日:将一些
floatDecimalCount
重命名为floatDigitCount
。在Decimal
类中,所有数字预期都是十进制的。还修复了余数/模测试以及正负十进制之间的比较; - 2023年8月8日:向示例应用程序添加了
MathParser
的更新版本。现在用户可以编写自己的表达式,而不仅仅是处理除法。表达式支持基本运算,如+ - * / %,以及一个名为“prior”的变量,用于获取上次执行的值,以及允许设置小数精度的Divide
和Power
方法; - 2023年8月7日:使用
BigInteger
并采用二进制/分治优化,自己实现了Power
; - 2023年8月5日:将“nominator”重命名为“numerator”。Denominator保持不变;
- 2023年8月5日:添加了
BigRational
的解释,并使其支持负数Power
/指数; - 2023年8月4日:初始版本。