Conversion between CGFloat and NSNumber without unnecessary promotion to Double

吃可爱长大的小学妹 提交于 2019-12-10 13:54:29

问题


As we all know, CGFloat (which is ubiquitous in CoreGraphics, UIKit etc) can be a 32-bit or 64-bit floating point number, depending on the processor architecture.

In C, CGFloat it is a typealias to float or double, in Swift is it defined as a struct CGFloat with a native property (which is Float or Double).

It has been observed repeatedly that a NSNumber can be created from and converted to Float and Double, but that there exist not similar conversions from and to CGFloat. The general advice (e.g. in Convert CGFloat to NSNumber in Swift) is to convert via Double

CGFloat <--> Double <--> NSNumber

Example:

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

and that is simple and correct, no precision is lost. Also most platforms are 64-bit nowadays, and then the CGFloat/Double conversion is trivial and probably optimized by the compiler.

However, it aroused my curiosity if a conversion can be done without promoting CGFloat to Double on 32-bit platforms.

One could use a build configuration statement (as e.g. in 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
    }
}

But what if Swift is ported to other architectures which are not Intel or ARM? This does not look very future proof.

One could also use the CGFLOAT_IS_DOUBLE constant (as e.g. in NSNumber from CGFloat):

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

The disadvantage here is that the compiler will always emit a "Will never be executed" warning on one of the cases.

So to make the long story short:

  • How can we convert between CGFloat and NSNumber in a safe way, without compiler warnings, and without unnecessary promotion to Double?

Please note that this is meant as an "academic" problem. As mentioned above (and in other Q&A's) one can simply convert via Double practically.

I am posting a "self-answer" here in the spirit of share your knowledge, Q&A-style. Of course other answers are welcome!


回答1:


Update: One can cast a CGFloat value to NSNumber and back:

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

This preserves the precision of CGFloat and works with Swift 2 and Swift 3.


(Previous answer – far too complicated): There are two solutions that I found. The first uses the toll-free bridging between NSNumber and CFNumber (as in What is most common and correct practice to get a CGFloat from an NSNumber? for Objective-C). It uses the fact that CFNumber has a dedicated conversion mode for CGFloat values:

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
    }
}

That is simple and nice. The only drawback: I could not figure out how to make the constructor an init method instead of a class method.

The second possible solution is a bit longer:

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())
    }
}

There are two private "helper" init methods with the same external parameter name doubleOrFloat but different parameter types. From the actual type of cgFloat.native the compiler determines which one to call in

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

Same idea in the accessor method. From the type of self.native the compiler determines which of the two doubleOrFloatValue() methods to call in

    var cgFloatValue : CGFloat {
        return CGFloat(floatLiteral: doubleOrFloatValue())
    }



回答2:


let num = NSNumber(double: 1.0)
let c: CGFloat? = CGFloat(exactly: num)

Here is the documentation for the initializer.



来源:https://stackoverflow.com/questions/34301579/conversion-between-cgfloat-and-nsnumber-without-unnecessary-promotion-to-double

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!