C#中decimal数据类型乘法的奇怪行为

35

我注意到在C#中,当乘以十进制数时有一种奇怪的行为。考虑以下乘法操作:

1.1111111111111111111111111111m * 1m = 1.1111111111111111111111111111 // OK
1.1111111111111111111111111111m * 2m = 2.2222222222222222222222222222 // OK
1.1111111111111111111111111111m * 3m = 3.3333333333333333333333333333 // OK
1.1111111111111111111111111111m * 4m = 4.4444444444444444444444444444 // OK
1.1111111111111111111111111111m * 5m = 5.5555555555555555555555555555 // OK
1.1111111111111111111111111111m * 6m = 6.6666666666666666666666666666 // OK
1.1111111111111111111111111111m * 7m = 7.7777777777777777777777777777 // OK
1.1111111111111111111111111111m * 8m = 8.888888888888888888888888889  // Why not 8.8888888888888888888888888888 ?
1.1111111111111111111111111111m * 9m = 10.000000000000000000000000000 // Why not 9.9999999999999999999999999999 ?

我不能理解的是上述两种情况中的后两种情况。那怎么可能呢?


11
欢迎来到精度误差的奇妙世界。 - christopher
这不是精度误差,只是四舍五入。 - Cody Gray
这里有一个数学谜题给你:10 / 9 = 1.111…; 1.111… * 9 = 9.999… ≠ 10. ;) - stakx - no longer contributing
2个回答

73

decimal 可以存储28或29个有效数字(96位)。基本上,尾数在-/+ 79,228,162,514,264,337,593,543,950,335的范围内。

这意味着当数值小于约7.9...时,你可以准确地获得29个有效数字 - 但超过该范围,则不行。这就是为什么8和9都会错,但较早的值不会出错的原因。通常情况下,你应该只依赖前28个有效数字,以避免出现奇怪的情况。

一旦将原始输入缩减为28个有效数字,就会获得预期的输出:

using System;

class Test
{
    static void Main()
    {
        var input = 1.111111111111111111111111111m;
        for (int i = 1; i < 10; i++)
        {
            decimal output = input * (decimal) i;
            Console.WriteLine(output);
        }
    }
}

我不确定您所说的“依赖于”28位数字的确切含义是什么。 您是否指明确舍入类似于除法结果的事物?Decimal使用十进制指数而不是二进制指数的事实意味着Decimal的名义数值将与其简洁字符串表示形式相匹配,但它是浮点类型的事实表明,使用Decimal进行相等测试很可能会遇到与任何其他浮点类型一样的问题。 - supercat
@supercat:我的意思是,如果你需要表示29位数字,你可能无法完全准确地表示,但你可以准确地表示多达28位数字。我认为依赖于“decimal”比依赖于“float”/“double”更合理——首先,你不需要担心某些操作的精度会因值是否在寄存器中而有所不同的可能性。它仍然必须要非常小心地处理,但在许多情况下,我认为这样做是可以的。 - Jon Skeet

2
数学家区分有理数和超集实数。有理数的算术运算是明确定义和精确的。而对于实数,使用加、减、乘、除等运算符进行算术运算只能在无理数以无理形式(符号)或者在某些表达式中可能转换成有理数时才能得到“精确”的结果。例如,根号2没有十进制(或其他有理基数)表示形式。然而,根号2乘以根号2就是有理数2。
计算机及其运行的语言通常只实现有理数,它们隐藏在诸如int、long int、float、double precision、real(FORTRAN)或其他暗示实数的名称后面。但所包含的有理数是有限的,不像有理数集合那样无限。
一个简单的例子,在计算机上找不到。1/2 * 1/2 = 1/4。如果你有一个有理数类,并且分子和分母的大小不超过整数算术的限制,则可以很好地工作。因此,(1,2) * (1,2) -> (1,4)。
但是,如果可用的有理数是十进制的并且限制为小数点后一位-虽然不实际,但代表了选择近似有理数(float / real等)实现时所做的选择,那么1/2将完全转换为0.5,然后0.5 + 0.5将等于1.0,但是0.5 * 0.5将必须是0.2或0.3!

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接