问题
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
andNSNumber
in a safe way, without compiler warnings, and without unnecessary promotion toDouble
?
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