为什么ToString()方法显示8位小数?

4
我有一个decimal类型的变量,其值为1.0。我将其保存到SQL Server 2012表中的列中,其类型为decimal(10, 8)
检索该值后,我发现它是1,但是当我调用ToString()方法时,返回的值是"1.00000000"(如下所示)。
我意识到8位小数对应于数据库中的数据类型。然而,在Entity Framework生成的属性中没有任何属性或任何东西可以给予它这种行为,因此我不知道这是如何发生的。
以下是我在Immediate窗口中进行的一些测试:
myDecimal
1
myDecimal.ToString()
"1.00000000"
myDecimal == 1
true
myDecimal == 1.0m
true

如您从最后两个测试中所看到的,这并不是浮点误差(尽管我并没有期望它,因为十进制是定点数,但我不得不尝试一下,因为我想不出其他的方法)。
您有任何想法吗? 十进制数据类型的 ToString() 如何产生带有8位小数的字符串? 编辑:作为比较,请看下面的测试。
1m.ToString()
"1"

2
@Tanner 这不是重复问题;我知道如果需要的话如何格式化字符串... 我想问的是为什么有这个默认的8位小数格式。 - Gigi
2
@Gigi,因为你的列定义指定了8位小数的精度,如果它们不全是零,那么删除它们将会失去精度。C#应该知道你想要将1.00000000表示为1.0而无需告诉它吗?如果是1.12345678,你是否期望C#默认返回1.1 - Tanner
2
@Gigi 它确实有小数位。例如,1.000代表的东西与1.0不同:1.000可能是1 +/- 0.0005,而1.0可能是1 +/- 0.05。 - Andrew Morton
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Tanner
1
@Tanner请注意,我的问题确切地涉及如何将十进制数(“不仅仅是一个数字”)在此情况下转换为特定格式的字符串,因此我的问题不能视为您指定的问题的重复。 - Gigi
显示剩余12条评论
4个回答

9

原因是十进制类型没有被规范化。同一个数字有多种表示方式,这些方式会被表示为不同的字符串。

这不是你的数据库类型特有的属性,这是十进制运作的正常方式。变量上没有特殊的DataAnotation或任何其他附加属性。

(1m).ToString() == "1"
(1.00000000m).ToString() == "1.00000000"
((1m)==(1.00000000m)) == true

对于一个双精度浮点数,只有一种有效表示方式,即mantissa * 2exponent的组合。
对于十进制数,mantissa * 10exponent可以有多种有效的表示方式。每个表示方式都代表同一个数字,但是通过多种可能的表示方式提供的额外信息用于在将十进制转换为字符串时选择默认的尾数位数。具体细节并没有得到很好的记录,我没有找到关于十进制数相加或相乘时指数具体发生了什么的信息。但它对ToString()的影响很容易验证。
缺点是Equals()和GetHashCode()操作比规范化的数字格式更复杂,并且在实现中存在微妙的错误:C# Why can equal decimals produce unequal hash values? Jon Skeet的这篇文章详细介绍了一些内容:
十进制数以128位存储,虽然只有102位是严格必要的。方便起见,可以将十进制数视为表示尾数的三个32位整数,然后是表示符号和指数的一个整数。最后一个整数的最高位是符号位(以通常的方式设置位(1)表示负数),16-23位(高16位中的低位)包含指数。其他位必须全部清除(0)。该表示法是由decimal.GetBits(decimal)给出的,它返回一个包含4个整数的数组。 …… 十进制类型不会将自己规范化——它记住了它有多少位小数(尽可能保持指数),在格式化时,0可以被视为重要的小数位。
您可以通过比较decimal.GetBits()返回的值来验证您拥有的两个十进制数是否相同:
decimal.GetBits(1m) == {int[4]}
    [0]: 1
    [1]: 0
    [2]: 0
    [3]: 0

decimal.GetBits(1.00000000m) == {int[4]}
    [0]: 100000000
    [1]: 0
    [2]: 0
    [3]: 524288

可能会有人倾向于依赖这种行为来格式化您的小数,但我建议在转换为字符串时始终明确选择精度,以避免混淆和意外惊喜,例如如果数字之前乘以一定的系数。


(1.00000000m).ToString() == "1.00000000" - 这应该是答案,其他的很容易得出结论。 - Sinatr
@Sinatr 显然到目前为止还没有。但如果这是答案,我当然不介意。我只是在更具影响力的方式上解释了十进制是如何工作的,而不是技术上的。=) - J. Steen
Jon Skeet的文章详细介绍了这个问题,甚至解释了在.NET 1.0中ToString()的行为曾经是不同的,但并没有提供更改的理由 - 你或其他人知道为什么选择了1.1及以后的行为吗? - Edouard Poor

5
当Entity Framework从查询结果中检索值时,它使用您的EDMX定义或数据注释来确定应在小数上设置什么精度。
由于您使用了decimal(10,8),因此Entity Framework会将您的小数设置为1.00000000。 十进制的ToString实现将尊重该精度并输出所有零,因为它们被认为是显着的。
使用的类型仍然是decimal。 您可以通过给定某个精确值来为十进制指定精度:1.000m1.0m更精确。 这只是十进制如何工作的方式,并在此处简要提到。 ToString无法知道您不认为零具有意义,除非您告诉它。 零仍然是一个值。

1
请问您能详细说明一下“在十进制数中,ToString的实现将尊重该精度”这部分吗?我找不到在哪里可以指定该精度。而且我也不明白EF使用了什么技术来实现这一点。作者指定了类型为“decimal”,是指“System.Decimal”还是其他类型? - Sinatr
这个简要的阐述足够了吗,还是说...?这个答案提供了更深入的技术解释,说明了十进制数值和精度是如何存储的。 - J. Steen

1
如果您正在使用数据库优先的EntityFramework,则edmx文件中将以xml格式保留列属性,如下所示...
<Property Name="ColumnName" Type="Decimal" Precision="8" Scale="4" Nullable="false" />

edmx文件提供有关属性的信息,就像DataAnotation一样。在您的情况下,列标记为Precision="8"。因此,当您调用该列中的ToString()方法时,字段将相应地格式化。

您可以使用任何标准数字格式字符串http://msdn.microsoft.com/en-us/library/dwhawy9k(v=vs.110).aspx或自定义数字格式字符串http://msdn.microsoft.com/en-us/library/0c899ak8(v=vs.110).aspx来格式化生成的字符串。

例如:

myDecimal.ToString("N") //1

我的问题是关于在这种情况下C# decimal的行为。 - Gigi
edmx文件提供有关属性的信息,就像DataAnotation一样。在您的情况下,列标记为Precision="8"。因此,当您在该列中调用ToString()方法时,该字段将相应地进行格式化。 - Teddy
啊,终于有点意义了。你能把那个编辑到你的答案里吗?否则我就不能改变我的投票了。 - Gigi
1
我不明白这个回答如何回答问题。XAML 如何影响 decimal.ToString()myDecimal 是不是除了 decimal 之外的其他类型? - Sinatr

0

你是对的,decimal(10,8) 告诉数据库返回 8 位小数。

如果将结果分配给数字类型,那就是一个数字,而且如果没有自己填充,它会是最小表示。

当你对数据库对象执行 ToString 时,它输出的正是从 SQL 服务器发送回来的内容,在本例中是带有 8 个零的 1。

您可以通过直接查询数据库服务器来确认此情况。


但是C#的十进制变量就像任何其他十进制一样。是什么让它在这种情况下以这种方式格式化自己? - Gigi
如果它来自数据库,那么它就是一个数据库对象,而不是十进制数。当您打印出该值时,会将其转换为实际的十进制数。ToString方法会输出从数据库中获取的内容。可以尝试在命令行上使用sqlcmd查看从数据库返回的内容。 - RobC
我知道它是如何从数据库返回的。问题在于我的变量是一个普通的C#十进制数,因此无论它是如何从数据库返回的都不能解释这种行为。 - Gigi

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