我们知道,Microsoft .NET Framework 中的 System.Decimal 结构(在 C# 语言中等价于 decimal 关键字)用来表示十进制数,范围从 -(296 - 1) 到 296 - 1,并且可以有 28 位小数。这就是说:
上面前两个是 decimal 的静态只读字段。遗憾的是,第三个不属于 decimal 结构。
decimal 内部使用 4 个 32-bit 的 System.Int32 来存储,占用 128 bits = 16 bytes。这 128 bits 分配如下:
decimal.GetBits 方法就返回上述 decimal 的内部表示。而 decimal (int[] bits) 构造函数就使用这个内部表示构造来构造 decimal 实例。一个 decimal 可能会有几种不同的内部表示,所有这些内部表示均同样有效,并且在数值上相等。
为了更好地理解 decimal 结构,我们来构造一个只有 8 bits = 1 byte 的 TinyDecimal 结构:
因此:
也就是说,TinyDecimal 的表示范围从 -63 至 63,并且可以有 1 位小数。
TinyDecimal 的正数有以下两种情形:
所以 TinyDecimal 的正数共有 63 + (63 - 6) = 120 个。负数的情况是一样的,也有 120 个。所以 TinyDecimal 有 241 个不同的值,即正数和负数各 120 个,加上一个零。注意,零有四种不同的表示:+0, -0, +0.0, -0.0。TinyDecimal 的正数顺序排列如下:
注意,在 TinyDecimal 中,6.3 的下一个数就是 7,7 的下一个数就是 8,根据就不存在 6.4 和 7.1 之类的数。并且有以下运算例子:
我们知道,1 byte 可以表示 28 = 256 个不同的值。而 TinyDecimal 有 241 个不同的值,计算如下:241 = 256 - 6 * 2 - 3 ,即需要扣除 6 * 2 个重复的正负数和 3 个重复的零。
System.Decimal 结构就是以上 TinyDecimal 结构的放大版本。为了更好地理解以上内容,我写了一个如下所测试程序:
1 using System; 2 3 static class DecimalTester 4 { 5 static void Main() 6 { 7 var epsilon = 0.0000000000000000000000000001m; 8 var a = decimal.MaxValue / 100; 9 var b = 7.1234567890123456789012345685m; 10 Console.WriteLine("{0}: 1e-28", epsilon); 11 Console.WriteLine("{0}: 1e-28 + 0.1", 0.1m + epsilon); 12 Console.WriteLine("{0}: a", a); 13 Console.WriteLine("{0,-30}: a + 0.004", a + 0.004m); 14 Console.WriteLine("{0,-30}: a + 0.005", a + 0.005m); 15 Console.WriteLine("{0,-30}: a + 0.01", a + 0.01m); 16 Console.WriteLine("{0,-30}: a + 0.099", a + 0.099m); 17 Console.WriteLine("{0,-30}: a + 0.1", a + 0.1m); 18 Console.WriteLine("{0,-30}: (a + 0.1) + 1e-28", a + 0.1m + epsilon); 19 Console.WriteLine("{0,-30}: a + (0.1 + 1e-28)", a + (0.1m + epsilon)); 20 Console.WriteLine("{0}: b", b); 21 Console.WriteLine("{0,-30}: b + 1", b + 1); 22 } 23 }
这个程序第 7 行的 epsilon 就是引言中提到的 decimal.Epsilon,其值为 10-28,等于 decimal 能够表示最小正数。
在 Arch Linux 64-bit 操作系统的 Mono 3.0.4 环境下编译和运行:
work$ dmcs --version Mono C# compiler version 3.0.4.0 work$ dmcs DecimalTester.cs work$ mono DecimalTester.exe 0.0000000000000000000000000001: 1e-28 0.1000000000000000000000000001: 1e-28 + 0.1 792281625142643375935439503.35: a 792281625142643375935439503.35: a + 0.004 792281625142643375935439503.4 : a + 0.005 792281625142643375935439503.4 : a + 0.01 792281625142643375935439503.4 : a + 0.099 792281625142643375935439503.5 : a + 0.1 792281625142643375935439503.5 : (a + 0.1) + 1e-28 792281625142643375935439503.5 : a + (0.1 + 1e-28) 7.1234567890123456789012345685: b 8.123456789012345678901234569 : b + 1
上述运行结果各行对应如下:
从上面的分析可以看出,在 Linux 的 Mono 环境中 decimal 的算术运算的舍入规则是四舍五入。
在 Windows 7 SP1 32-bit 操作系统的 Microsoft .NET Framework 4.5 环境下编译和运行:
D:\work> csc DecimalSumTester.cs Microsoft(R) Visual C# 编译器版本 4.0.30319.17929 用于 Microsoft(R) .NET Framework 4.5 版权所有 (C) Microsoft Corporation。保留所有权利。 D:\work> DecimalTester 0.0000000000000000000000000001: 1e-28 0.1000000000000000000000000001: 1e-28 + 0.1 792281625142643375935439503.35: a 792281625142643375935439503.35: a + 0.004 792281625142643375935439503.4 : a + 0.005 792281625142643375935439503.4 : a + 0.01 792281625142643375935439503.4 : a + 0.099 792281625142643375935439503.4 : a + 0.1 792281625142643375935439503.4 : (a + 0.1) + 1e-28 792281625142643375935439503.5 : a + (0.1 + 1e-28) 7.1234567890123456789012345685: b 8.123456789012345678901234568 : b + 1
上述运行结果各行对应如下:
从上面的分析可以看出,在 Windows 的 .NET Framework 环境中 decimal 的算术运算的舍入规则是四舍六入五取偶。所以造成第 8 、9 和 12 行和 Linux 中的输出不同。
由于 decimal 的精度是有限的,只能表示有限个分散的值,在进行一些特殊的算术运算步骤时,会产生非常出乎意料的结果。且听下回分解。