Swift: 将字符串转换为双精度浮点数时出现问题

19

这是一个在 Xcode 7.3.1 playground 中的简单代码:

var str = "8.7" print(Double(str))

输出结果令人惊讶: Optional(8.6999999999999993)

同时,Float(str) 的输出结果为:8.69999981

对此有何思考或原因? 如果有相关参考资料,请提供。

此外,我应该如何将"8.7"转换成Double(或Float)类型的8.7?

编辑

在 Swift 中:

(str as NSString).doubleValue 返回 8.7

现在,问题的答案已经找到了替代方案,但我们为什么不能依赖于Double("8.7")呢?请深入探讨一下。

编辑2

("6.9" as NSString).doubleValue // 输出结果为6.9000000000000004

所以,这个问题又被提出来了。


4
如果您的问题是关于8.7与8.69999981之间的区别,那么您应该阅读“浮点数计算是否存在问题?”和“计算机科学家应该了解的浮点运算知识”这两篇文章。链接分别为:https://dev59.com/G3RB5IYBdhLWcg3wj36c 和 http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html。 - Martin R
1
@l'L'l,Martin 但是为什么将“8.7”转换为Double不会得到8.7而是8.69999999999991。这样做有什么意义呢? 如果这确实是在硬件级别上进行的操作,那么Swift应该已经处理了它。(例如,它将其转换为.doubleValue的8.7,但不适用于Double(“8.7”)) - v-i-s-h-a-l
@l'L'l,那么为什么(str as NSString)。doubleValue会返回8.7。它应该返回类似于8.699999999991的东西。 - v-i-s-h-a-l
它显然会自动将其转换为%.1f。您可以通过将值设置为X.X进行测试,如果出错,它应该返回0.0... - l'L'l
2
这个问题最初比我想象的更有趣... - Martin R
显示剩余4条评论
2个回答

26

这里有两个不同的问题。第一个——正如在评论中已经提到的那样——二进制浮点数不能精确表示数字8.7。Swift使用IEEE 754标准来表示单精度和双精度浮点数,如果您进行赋值操作,会发生舍入错误。

let x = 8.7

那么最接近的可表示数字将存储在x中,即为

8.699999999999999289457264239899814128875732421875

关于这个问题,更多信息可以在优秀的问答页面Is floating point math broken?中找到。


第二个问题是:为什么有时候这个数字会被打印成"8.7",有时候会被打印成"8.6999999999999993"?

let str = "8.7"
print(Double(str)) // Optional(8.6999999999999993)

let x = 8.7
print(x) // 8.7

Double("8.7")8.7有什么不同?它们哪一个更精确呢?

为了回答这些问题,我们需要了解print()函数的工作方式:

  • 如果一个参数符合CustomStringConvertible,那么print函数调用它的description属性并将结果打印到标准输出。
  • 否则,如果一个参数符合CustomDebugStringConvertible,那么print函数调用它的debugDescription属性并将结果打印到标准输出。
  • 否则,使用其他机制。 (这里未导入,与我们的目的无关。)

Double类型符合CustomStringConvertible,因此

let x = 8.7
print(x) // 8.7

生成与

let x = 8.7
print(x.description) // 8.7

但是在发生什么事情呢?

let str = "8.7"
print(Double(str)) // Optional(8.6999999999999993)

Double(str) 是一个可选类型,而struct Optional 不符合CustomStringConvertible协议,但符合CustomDebugStringConvertible协议。因此,打印函数调用OptionaldebugDescription属性,进而调用底层DoubledebugDescription属性。因此,除了是可选类型以外,输出的数字与常规情况下一样。

let x = 8.7
print(x.debugDescription) // 8.6999999999999993

但是对于浮点数,descriptiondebugDescription之间有什么区别呢?从Swift源代码中可以看到,两者最终都调用了Stubs.cpp中的swift_floatingPointToString函数,并分别将Debug参数设置为falsetrue。这控制了数字转换成字符串的精度:

  int Precision = std::numeric_limits<T>::digits10;
  if (Debug) {
    Precision = std::numeric_limits<T>::max_digits10;
  }

有关这些常量的含义,请参见http://en.cppreference.com/w/cpp/types/numeric_limits

  • digits10 - 可表示的十进制数字位数,不会发生变化,
  • max_digits10 - 必要的十进制数字位数以区分此类型的所有值。

因此,description创建的字符串小于最大精度。该字符串可以转换为Double,然后再次转换为字符串,得到相同的结果。debugDescription创建一个具有更高精度的字符串,因此任何两个不同的浮点值都将产生不同的输出。


摘要:

  • 大多数十进制数不能准确地表示为二进制浮点值。
  • 浮点类型的descriptiondebugDescription方法在转换为字符串时使用不同的精度。因此,
  • 打印可选浮点值与打印非可选值使用不同的精度进行转换。

因此,在打印之前,您可能需要取消封送可选项:

let str = "8.7"
if let d = Double(str) {
    print(d) // 8.7
}

为了更好的控制,可以使用 NSNumberFormatter 或者使用带有 %.<precision>f 格式的格式化打印。

另一个选择是使用 (NS)DecimalNumber 代替 Double (例如用于货币金额),参见例如Swift中的舍入问题


请您看一下问题的最后编辑。 - v-i-s-h-a-l
2
@v-i-s-h-a-l: 在我的测试中,("6.9" as NSString).doubleValue 输出 "6.9"。但即使如此,除非使用数字格式化程序,否则不能依赖于特定的输出。 - Martin R

3

我会使用:

let doubleValue = NSNumberFormatter().numberFromString(str)?.doubleValue

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