【问题标题】:Convert float to double loses precision but not via ToString将浮点数转换为双精度会丢失精度,但不会通过 ToString
【发布时间】:2011-08-24 15:00:06
【问题描述】:

我有以下代码:

float f = 0.3f;
double d1 = System.Convert.ToDouble(f);
double d2 = System.Convert.ToDouble(f.ToString());

结果等价于:

d1 = 0.30000001192092896;
d2 = 0.3;

我很想知道这是为什么?

【问题讨论】:

标签: c# string floating-point double precision


【解决方案1】:

这不是精度损失 .3 不是representable in floating point。当系统转换为字符串时,它会四舍五入;如果你打印出足够多的有效数字,你会得到更有意义的东西。

为了看得更清楚

float f = 0.3f;
double d1 = System.Convert.ToDouble(f);
double d2 = System.Convert.ToDouble(f.ToString("G20"));

string s = string.Format("d1 : {0} ; d2 : {1} ", d1, d2);

输出

"d1 : 0.300000011920929 ; d2 : 0.300000012 "

【讨论】:

  • 啊哈,这是有道理的,所以默认的 ToString 方法只是截断输出,舍入(并且在技术上使其更不精确)。但舍入允许我检索我设置的初始值。
  • +1!两个问题... 转换为字符串时浮点数被舍入到(多少位数)?更重要的是,为什么?如果有人使用浮点数并且他们分配了一个值,但由于浮点数限制而没有存储确切的值,那么到底为什么 ToString 会决定为你取整?更糟糕的是,因为调试器输出当然做同样的事情,所以像 (float)0.3 这样的东西仍然在调试输出中显示 0.3,你永远不会意识到你正在失去那个精度。这太愚蠢了。
  • 它就是所有浮点的工作方式。只有这么多位可以疯狂地表示许多实数。有一个错误 epsilon 和显示逻辑知道,当 0.3 左右的 epsilon 足够低以显示 0.3 时。具体在链接中
【解决方案2】:

您不会失去精确度;您正在从不太精确的表示(浮点,32 位长)向上转换为更精确的表示(双精度,64 位长)。你得到的更精确的表示(超过某个点)只是垃圾。如果你将它从一个双精度浮点数转换回浮点数,你将拥有与以前完全相同的精度。

这里发生的情况是您为浮点数分配了 32 位。然后,您向上转换为双精度数,再添加 32 位来表示您的数字(总共 64 位)。这些新位是最不重要的(小数点右边最远的),并且与实际值无关,因为它们以前是不确定的。结果,这些新位具有您进行向上转换时碰巧具有的任何值。它们和以前一样不确定——换句话说,垃圾。

当您从 double 向下转换为 float 时,它会去掉那些最低有效位,留下 0.300000(7 位精度)。

从字符串转换为浮点数的机制不同;编译器需要分析字符串'0.3f'的语义并找出它与浮点值的关系。它不能像浮点/双精度转换那样通过位移来完成——因此,这是您期望的值。

有关浮点数如何工作的更多信息,您可能有兴趣查看有关 IEEE 754-1985 标准的 this 维基百科文章(其中包含一些方便的图片和对事物机制的良好解释),以及 @ 987654322@wiki文章2008年标准更新。

编辑:

首先,正如@phoog 在下面指出的那样,从浮点数向上转换为双精度数并不像在为记录数字而保留的空间中添加另外 32 位那么简单。实际上,您将为指数增加 3 位(总共 11 位),并为分数增加 29 位(总共 52 位)。加上符号位,你总共得到了 64 位作为双精度位。

此外,暗示在那些最不重要的位置存在“垃圾位”是一个粗略的概括,并且可能不适用于 C#。一些解释和下面的一些测试向我表明,这对于 C#/.NET 是确定性的,并且可能是转换中某些特定机制的结果,而不是保留内存以提高精度。

回到以前,当您的代码编译成机器语言二进制文件时,编译器(至少是 C 和 C++ 编译器)不会在您保留时添加任何 CPU 指令来“清除”或初始化内存中的值变量的空间。因此,除非程序员将变量显式初始化为某个值,否则为该位置保留的位的值将保持它们在您保留该内存之前的任何值。

在 .NET 领域,您的 C# 或其他 .NET 语言编译成中间语言(CIL,通用中间语言),然后由 CLR 即时编译以作为本机代码执行。 C# 编译器或 JIT 编译器可能会或可能不会添加变量初始化步骤;我不知道。

以下是我所知道的:

  • 我通过将浮点数转换为三个不同的双精度来测试这一点。每个结果都具有完全相同的值。
  • 该值与上面@rerun 的值完全相同:double d1 = System.Convert.ToDouble(f); 结果:d1 : 0.300000011920929
  • 如果我使用double d2 = (double)f; 进行投射,我会得到相同的结果结果:d2 : 0.300000011920929

我们三个人获得相同的值,看起来向上转换的值是确定性的(实际上不是垃圾位),表明 .NET 在我们所有的机器上都以相同的方式做某事 .仍然可以说,附加数字的精确度不会比以前高或低,因为 0.3f 并不完全等于 0.3——它等于 0.3,精度高达 7 位。我们对前七位以外的其他数字的值一无所知。

【讨论】:

  • 谢谢乔,这里有一些很棒的信息,我了解第一行中的浮点与双精度转换,主要问题是了解第二行中发生了什么以实现我正在寻找的结果。谢谢!
  • 关于最低有效位的那一点是不正确的(至少在 C# 中不是)。首先,浮点数不仅仅是去掉了 32 位的双精度数;用于指定指数的位数不同,指数偏差也不同。其次,如果它为真,就不可能从浮点数到双精度数往返往返一致。
  • 您说得有道理,它不像添加额外的 32 位那么简单;我会修改我的答案以反映这一点。不过,我不确定 C# 中的垃圾位;虽然 .NET 将针对 CLR 而不是本机运行,但我对 CLR 的运行方式知之甚少,无法知道当您进行这样的向上转换时它是否会清除/归零 29 个最低有效位。你有什么资源可以推荐吗?
【解决方案3】:

在这种情况下和其他情况下,我使用十进制转换来获得正确的结果

float ff = 99.95f;
double dd = (double)(decimal)ff;

【讨论】:

  • 警告:这可能会引发 OverflowException!
  • 这可能比 ToString() 解决方案性能更高! +-10^28 的范围对我来说是可以的。
  • +-7.922816E27 更安全。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-05-26
  • 1970-01-01
  • 2013-06-17
  • 2015-06-12
  • 1970-01-01
  • 2014-09-28
  • 1970-01-01
相关资源
最近更新 更多