swift中is的小问题

在swift中用到is者个关键字,结果发现了其中一个莫名其妙的小问题

import UIKit

let arr = [1, "a", 2.88]
let last = arr.last
if last is Int {
    print("The last is Int.")    // The last is Int.
} else {
    print("The last is not Int.")
}

数组后面明明是一个Double类型,可是通过is判断,打印出的结果却是The last is Int.而且不论最后一个是Double或者是Int都会显示同样的结果.但是如果是String就不会显示.
表示很纳闷,请教了刚哥,刚哥也比较困惑,于是把这个问题放到了stackoverflow上.
没多久得到一个教科书般的回复.

回复一

Swift arrays can only hold one type. When you declared:

let arr = [1, "a", 2.88]

Swift made arr of type [NSObject]. You can verify this by Option-clicking on arr to see its type. This only works because you have Foundation imported (your import UIKit imports Foundation as well). Try removing import UIKit.

Then, the values 1 and 2.88 were converted to NSNumber and "a" to NSString so that they can be stored in that[NSObject]array because Ints, Strings, and Doubles are not NSObjects. NSNumber and NSString are subclasses of NSObject. Swift picks the most restrictive type for the array. Had your array been[1, true, 2.88], the array type would have been [NSNumber].

The interesting thing about an NSNumber is that it is an object container that wraps many different types. You can put an Int in and take out a Double. So, it is misleading then when you test it with is. It responds "true" meaning, "I can be that if you want".

import Foundation

let n: NSNumber = 3.14

print(n is Int)       // "true"
print(n is Double)    // "true"
print(n is Bool)      // "true"

print(n as! Int)      // "3"
print(n as! Double)   // "3.14"
print(n as! Bool)     // "true"

Note: Had you declared your arr to be [Any], then this would have worked as you expected:

let arr:[Any] = [1, "a", 2.88]
let last = arr.last
if last is Int {
    print("The last is Int.")
} else {
    print("The last is not Int.")  // "The last is not Int."
}

But Swift is not going to create an array of type Any for you unless you ask explicitly (because quite frankly it is an abomination in a strongly typed language). You should probably rethink your design if you find yourself using[Any].

The only reason Swift creates [NSObject] when Foundation is imported is to make our lives easier when calling Cocoa and Cocoa Touch APIs. If such an API requires an NSArray to be passed, you can send [1, "a", 2.88] instead of [NSNumber(integer: 1), NSString(string: "a"), NSNumber(double: 2.88)].

回复二

When you have imported Foundation.frameworkyou inherit a certain magic behaviour with numbers: if you create array literal with numerical literals that can't be represented with an array of that value type, an array is created that wraps the numbers boxed in a NSNumber. If you don't import Foundation (i.e. you rely on just the Swift standard library), then you in fact can't declare the array in such cases just with let arr = [1, "a", 2.88] (whereas for instance let arr =[1,2] would still work – that would produce an[Int]) but need to statelet arr:[Any] = [1, "a", 2.88]– at which case no boxing to NSNumber happens (NSNumber is not even available) and the original type is somehow retained.

NSNumber is a box that can be used to wrap all manners of scalar (numerical) basic types, and you can use the as or is operator in Swift to successfully treat any NSNumber as any of the numerical types it can wrap. For instance the following holds:

var a = NSNumber(bool: true)
print("\(a is Bool)")   // prints "true"
print("\(a is Int)")    // prints "true"
print("\(a is Double)") // prints "true"
print("\(a is Float)")  // prints "true"

It is possible to figure out the kind of numerical type you used sometimes, but it's just not exposed for some reason to NSNumber but is only available in the corresponding CoreFoundation type CFNumber:

extension NSNumber {
    var isBoolean:Bool {
        return CFNumberGetType(self as CFNumber) == CFNumberType.CharType
    }

    var isFloatingPoint:Bool {
        return CFNumberIsFloatType(self as CFNumber)
    }

    var isIntegral:Bool {
        return CFNumberGetType(self as CFNumber).isIntegral
    }
}

extension CFNumberType {
    var isIntegral:Bool {
        let raw = self.rawValue
        return (raw >= CFNumberType.SInt8Type.rawValue && raw <= CFNumberType.SInt64Type.rawValue)
                || raw == CFNumberType.NSIntegerType.rawValue
                || raw == CFNumberType.LongType.rawValue
                || raw == CFNumberType.LongLongType.rawValue
    }
}

You can read more on this topic over here – there are some caveats.

Note also that the reference documentation also states the following:

… number objects do not necessarily preserve the type they are created with …

结论

看了上面的回复,总算搞清楚了.像我上面的代码,导入Foundation框架后创建出来的数组,1,2.88会自动包装为NSNumber,而这个NSNumber会和Bool,Int,Double,Float匹配,所以返回true.

老外解释的非常详细,不止是提出了问题的原因所在,甚至给出了解决方案.
面对这样的答案我只能说,路漫漫其修远兮,态度很重要...

推荐阅读更多精彩内容