【问题标题】:Safe conversion of Float to Int?Float 到 Int 的安全转换?
【发布时间】:2019-03-31 19:24:47
【问题描述】:

Float 到 Int 的转换被记录为很简单:

let i = Int(x)

但仅此一点是不安全的,因为如果 x 太大而无法放入整数,或者是某种 NaN,Swift 应用程序将会崩溃。

那么,将 Float 或 Double 的未知内容转换为 Int(Int32、UInt16 等)的最简单但安全的方法是什么?没有崩溃的风险?有 Int?() 类型吗?还是等效的“if let”语句?

【问题讨论】:

  • 如果浮点数超过Int.max,您期望什么值? nilInt.max?
  • Int.min 或 nil 比中止应用程序更安全。
  • @ielyamani nil 会是更好的设计。这将解释留给消费者。如果他们想要Int.max,可以很容易地只是?? Int.max,但这使他们能够知道号码何时超出范围。

标签: swift type-conversion numbers


【解决方案1】:

Int(exactly:) 可能就是您要找的东西:

如果可以精确表示,则从给定的浮点值创建一个整数。

如果作为源传递的值不能精确表示,则结果为零。

例子:

let x = 123e20
if let i = Int(exactly: x) {
    print(i)
} else {
    print("not representable")
}

如果浮点数不是整数,这也将失败,因此您可能需要在转换之前对其进行舍入:

let x = 12.3
if let i = Int(exactly: x.rounded(.towardZero)) {
    print(i)
}

向零舍入是Int(x) 所做的,你可以选择你想要的舍入模式。

【讨论】:

    【解决方案2】:

    Martin R 的回答显示了正确的方法,但我仍在写这篇文章,以了解“幕后”发生了什么。

    Double 的有限精度意味着只有在Int64.maxInt64.min 的量级上,Double 表示只能用于每个4,096 整数。结果,有一组整数,它们是有效的,并且在Int64 的范围内,在(有损)转换为 Double 之后,最终舍入到一个不再可以表示为Int64 的大小。为了考虑这些值,我们需要确保我们只接受Double(Self.min).nextUp ... Double(Self.max).nextDown,而不是Double(Self.min)... Double(Self.max)

    Int.min                 -9,223,372,036,854,775,808 
    Float(Int.min)          -9,223,372,036,854,780,000 lower than Int.min by 4096, thus not representable by Int
    Float(Int.min).nextUp   -9,223,371,487,098,960,000 greater than Int.min by 549,755,820,032, thus representable by Int
    Int.max                 +9,223,372,036,854,775,807  
    Float(Int.max)          +9,223,372,036,854,780,000 greater than Int.max by 4096, thus not representable by Int
    Float(Int.max).nextDown +9,223,371,487,098,960,000 lower than Int.max by 549,755,820,032, thus representable by Int
    

    这就是实际的样子

    import Foundation
    
    extension FixedWidthInteger {
        static var representableDoubles: ClosedRange<Double> {
            return Double(Self.min).nextUp ... Double(Self.max).nextDown
        }
    
        init?(safelyFromDouble d: Double) {
            guard Self.representableDoubles.contains(d) else { return nil }
            self.init(d)
        }
    }
    
    func formatDecimal(_ d: Double) -> String{
        let numberFormatter = NumberFormatter()
        numberFormatter.numberStyle = .decimal
        numberFormatter.positivePrefix = "+"
        return numberFormatter.string(from: NSNumber(value: d))!
    }
    
    let testCases: [Double] = [
        Double.nan,
        -Double.nan,
        Double.signalingNaN,
        -Double.signalingNaN,
    
        Double.infinity,
        Double(Int.max),
        Double(Int.max).nextDown,
        +1,
        +0.6,
        +0.5,
        +0.4,
        +0,
        -0,
        -0.4,
        -0.5,
        -0.6,
        -1,
        -1.5,
        Double(Int.min).nextUp,
        Double(Int.min),
        -Double.infinity,
    ]
    
    for d in testCases {
        print("Double: \(formatDecimal(d)), as Int: \(Int(safelyFromDouble: d)as Any)")
    }
    

    哪个打印:

    Double: NaN, as Int: nil
    Double: NaN, as Int: nil
    Double: NaN, as Int: nil
    Double: NaN, as Int: nil
    Double: +∞, as Int: nil
    Double: +9,223,372,036,854,780,000, as Int: nil
    Double: +9,223,372,036,854,770,000, as Int: Optional(9223372036854774784)
    Double: +1, as Int: Optional(1)
    Double: +0.6, as Int: Optional(0)
    Double: +0.5, as Int: Optional(0)
    Double: +0.4, as Int: Optional(0)
    Double: +0, as Int: Optional(0)
    Double: +0, as Int: Optional(0)
    Double: -0.4, as Int: Optional(0)
    Double: -0.5, as Int: Optional(0)
    Double: -0.6, as Int: Optional(0)
    Double: -1, as Int: Optional(-1)
    Double: -1.5, as Int: Optional(-1)
    Double: -9,223,372,036,854,770,000, as Int: Optional(-9223372036854774784)
    Double: -9,223,372,036,854,780,000, as Int: nil
    Double: -∞, as Int: nil
    

    【讨论】:

    • @Alexander 精度仍然丢失。 Int.min...Int.max 中的整数值与其使用 Float 或 Double 的图像之间没有双射。 (rot13: Abg fher jul guvf tbg qbjaibgrq)
    • @MartinR 故意的。 “但我仍然在写这篇文章,以教导“幕后”发生的事情。
    • @ielyamani 是的,这段代码对 0 进行了一些截断(如 0.60.50.4 测试用例所示),但它确实阻止了初始化整数的操作。范围双打。
    【解决方案3】:

    我认为nil 将是Float.nanFloat.infinityInt.min...Int.max 范围之外转换的适当返回值。

    您可以定义应包含浮点数的范围:

    let validRange: ClosedRange<Float> = Float(Int.min)...Float(Int.max)
    

    然后像这样使用它:

    func convert(_ f: Float) -> Int? {
        var optInteger: Int? = nil
    
        if  validRange.contains(f) {
            optInteger = Int(f)
        }
    
        return optInteger
    }
    
    print(convert(1.2))  //Optional(1)
    

    您应该考虑到FloatInt 之间的转换会因大值而失去精度,因为每种类型的位数有限。例如,使用@MartinR 的Int.init(exactly:) 不够精确:

    let x: Float = 9223371487098961919.0
    if let i = Int(exactly: x.rounded()) {
        print(i) 
    }
    

    产量

    9223371487098961920

    一个可能的解决方案是使用更精确的类型。例如Float80

    let validRange: ClosedRange<Float80> = Float80(Int.min)...Float80(Int.max)
    
    func convert(_ f80: Float80) -> Int? {
        var optInteger: Int? = nil
    
        if  validRange.contains(f80) {
            optInteger = Int(f80)
        }
    
        return optInteger
    }
    

    以下是一些用例:

    convert(Float80(Int.max))        //Optional(9223372036854775807)
    convert(Float80(Int.max) - 1.0)  //Optional(9223372036854775806)
    convert(Float80(Int.max) + 1.0)  //nil
    convert(Float80(Int.min)))       //Optional(-9223372036854775808)
    convert(Float80(Int.min) - 1.0)) //nil
    convert(Float80.infinity)        //nil
    convert(Float80.nan)             //nil
    

    【讨论】:

    • 这实际上迫使您使用Float80,因为第一个代码根本不适用于FloatDoubleFloat(Int.max)9,223,372,036,854,780,000,比4193 多于Int.max (9,223,372,036,854,775,807),这意味着有4193 正整数值,contains 检查将成功,通过整数转换将崩溃。
    • 9223371487098961919.0 到 Int 的转换不会丢失精度,因为 Float(或 Double)一开始就没有这种精度:print(9223371487098961919.0 - 9223371487098961920.0) // 0
    • @MartinR 您的评论基本上在答案中提到:9223371487098961919 是故意选择的
    • @Alexander 移至 Float80 是答案中的逻辑推论。在之前的编辑中,您会看到一个错误示例。
    • 如果您要使用Float80 路由,那么我会将其保留在API 内部(获取Double 类型的参数,并在内部执行Float80(d))。更好的是,您可以仅使用 Double 解决此问题。看我的回答
    猜你喜欢
    • 2016-09-01
    • 2011-09-28
    • 2018-03-11
    • 2022-11-30
    • 2021-01-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多