CGFloat 和 NSNumber 之间的转换,无需不必要地提升为 Double

Conversion between CGFloat and NSNumber without unnecessary promotion to Double

众所周知,CGFloat(在 CoreGraphics、UIKit 等中无处不在) 可以是 32 位或 64 位浮点数,具体取决于 处理器架构。

在 C 中,CGFloat 它是一个类型别名 到 floatdouble,在 Swift 中它被定义为一个 struct CGFloat 与 a native 属性(即 FloatDouble)。

反复观察到 NSNumber 可以从 并转换为 FloatDouble,但不存在 与 CGFloat 之间的类似转换。一般建议 (例如 )是为了 通过 Double

转换
CGFloat <--> Double <--> NSNumber

示例:

let c1 = CGFloat(12.3)
let num = NSNumber(double: Double(c1))
let c2 = CGFloat(num.doubleValue)

这很简单而且正确,没有精度损失。 现在大多数平台都是 64 位的,然后 CGFloat/Double 转换很简单,可能由编译器优化。

但是,是否可以进行转换,这引起了我的好奇 没有 在 32 位平台上将 CGFloat 提升为 Double

可以使用 构建配置语句(例如 Should conditional compilation be used to cope with difference in CGFloat on different architectures?):

extension NSNumber {
    convenience init(cgFloatValue value : CGFloat) {
        #if arch(x86_64) || arch(arm64)
            self.init(double: value.native)
        #else
            self.init(float: value.native)
        #endif
    }
}

但是如果 Swift 被移植到其他不是 英特尔还是ARM?这看起来不太适合未来。

也可以使用 CGFLOAT_IS_DOUBLE 常量(例如 NSNumber from CGFloat):

    if CGFLOAT_IS_DOUBLE != 0 {
        // ...
    } else {
        // ...
    }

这里的缺点是编译器总是发出一个 “永远不会被执行” 对其中一种情况的警告。

长话短说:

请注意,这是一个“学术”问题。如前所述 上面(以及其他问答中)可以通过 Double 简单地转换 实际上。

我本着 share your knowledge, Q&A-style。当然也欢迎其他答案!

更新: 可以将 CGFloat 值转换为 NSNumber 并返回:

let c1 = CGFloat(12.3)
let num = c1 as NSNumber
let c2 = num as CGFloat

这保留了 CGFloat 的精度并适用于 Swift 2 和 Swift 3.


(上一个答案 - 太复杂了): 我找到了两个解决方案。第一个使用免费桥接 在 NSNumberCFNumber 之间(如 What is most common and correct practice to get a CGFloat from an NSNumber? Objective-C)。它利用了 CFNumber 一个专用的事实 CGFloat 个值的转换模式:

extension NSNumber {

    // CGFloat -> NSNumber
    class func numberWithCGFloat(var value: CGFloat) -> NSNumber {
        return CFNumberCreate(nil , .CGFloatType, &value)
    }

    // NSNumber -> CGFloat
    var cgFloatValue : CGFloat {
        var value : CGFloat = 0
        CFNumberGetValue(self, .CGFloatType, &value)
        return value
    }
}

这很简单也很好。唯一的缺点:我想不通 如何使构造函数成为 init 方法而不是 class method.

第二种可能的解决方案有点长:

extension NSNumber {

    // CGFloat -> NSNumber
    private convenience init(doubleOrFloat d : Double) {
        self.init(double : d)
    }
    private convenience init(doubleOrFloat f : Float) {
        self.init(float : f)
    }
    convenience init(cgFloat : CGFloat) {
        self.init(doubleOrFloat: cgFloat.native)
    }

    // NSNumber -> CGFloat
    private func doubleOrFloatValue() -> Double {
        return self.doubleValue
    }
    private func doubleOrFloatValue() -> Float {
        return self.floatValue
    }
    var cgFloatValue : CGFloat {
        return CGFloat(floatLiteral: doubleOrFloatValue())
    }
}

有两个私有 "helper" 初始化方法具有相同的外部 参数名称 doubleOrFloat 但参数类型不同。从实际 cgFloat.native 的类型由编译器决定调用哪一个 在

    convenience init(cgFloat : CGFloat) {
        self.init(doubleOrFloat: cgFloat.native)
    }

访问器方法中的想法相同。来自 self.native 的类型 编译器确定两者中的哪一个 doubleOrFloatValue() 调用方法

    var cgFloatValue : CGFloat {
        return CGFloat(floatLiteral: doubleOrFloatValue())
    }
let num = NSNumber(double: 1.0)
let c: CGFloat? = CGFloat(exactly: num)

这里是 documentation 初始化程序。