Objective-C id 作为 Swift Any
Swift 3 与 Objective-C 的 API 接口比以前的版本更好用了。比如说,Swift 2 把 Objective-C 中的 id 映射为 Swift 中的 AnyObject ,它一般能储存类类型的值。Swift 2 同样为一些桥接的值类型提供了隐式的 AnyObject ,比如说 String 、 Array 、 Dictionary 、 Set 以及某些数值,作为一种 Swift 原生类型可以方便地应用在 Cocoa API 上的便捷方式,比如 NSString 、 NSArray 、或者其他来自 Foundation 的容器类型。这些转换与其他语言不一致,这就导致了让人很难理解究竟什么被用作了 AnyObject ,从而玩出 Bug。
在 Swift 3 中,Objective-C 中的 id 类型现在映射为 Swift 中的 Any ,它描述了一个任意类型的值,无论是类、枚举、结构体还是其他 Swift 类型。这个变化致使 Objective-C API 在 Swift 中更加灵活,因为 Swift 中定义的值类型可以传送到 Objective-C API 中并作为 Swift 类型展开,消除了手动“包装”类型的需求。这些好处同时扩展了集合:Objective-C 集合类型 NSArray 、 NSDictionary 以及 NSSet ,它先前只接受 AnyObject ,现在可以包含 Any 类型。对于哈希容器,比如 Dictionary 和 Set ,现在有新的类型 AnyHashable 能保存任何遵循 Swift 中 Hashable 协议的类型。总的来说,从 Swift 2 到 Swift 3 的类型映射改动如下表:
Objective-C | Swift 2 | Swift 3 |
---|---|---|
id | AnyObject | Any |
NSArray * | [AnyObject] | [Any] |
NSDictionary * | [NSObject: AnyObject] | [AnyHashable: Any] |
NSSet * | Set<NSObject> | Set<AnyHashable> |
很多情况下,你的代码就不会受到这个改变的影响。Swift 2 中隐式转换为 AnyObject 的代码在 Swift 3 中还是会照常作为Any运作。总之,还是有一些地方比如你声明的变量和方法需要改变并在 Swift 3 中获得最好的体验。同时,从 Swift 3 开始,在对象和值类型不能再隐式转换,如果你的代码显式地使用 AnyObject 或者 Cocoa 类比如 NSString 、 NSArray 或者 NSDictionary ,你就得引入更多显式地使用 as NSString 或者 as String 转换。Xcode 中的自动迁移工具在你从 Swift 2 迁移到 Swift 3 时将做最小的改动来保证你的代码可编译,但结果可能不能总是完美。这篇文章将描述一些你需要做的改动,以及一些获得 id 作为 Any 优势时需要规避的陷阱。
重写方法和遵循协议
当子类化一个 Objective-C 类并且重写它的方法、或者说遵循一个 Objective-C 协议时,在父类方法在 Objective-C 中使用时方法的类型特征就需要更新。某些通用的例子是 NSObject 类的 isEqual: 方法以及 NSCopying 协议的 copyWithZone: 方法。在 Swift 2 中,你可能会写一个 NSObject 的子类以遵循 NSCopying 比如这样:
// Swift 2 class Foo: NSObject, NSCopying { override func isEqual(_ x: AnyObject?) -> Bool { ... } func copyWithZone(_ zone: NSZone?) -> AnyObject { ... } }
在 Swift 3,为了做从 copyWithZone(_:) 到 copy(with:) 命名改变,你同样需要改变这些方法的特征来使用 Any 而不是 AnyObject :
// Swift 3 class Foo: NSObject, NSCopying { override func isEqual(_ x: Any?) -> Bool { ... } func copy(with zone: NSZone?) -> Any { ... } }
无类型集合
属性列表,JSON以及用户信息字典在 Cocoa 中是通用的,并且 Cocoa 原生地把这些表示为无类型集合。在 Swift 2 中,必须用 AnyObject 或者 NSObject 元素来建立 Array 、 Dictionary 或者 Set ,依托于隐式绑定转换来处理值类型:
// Swift 2 struct State { var name: String var abbreviation: String var population: Int var asPropertyList: [NSObject: AnyObject] { var result: [NSObject: AnyObject] = [:] // Implicit conversions turn String into NSString here… result["name"] = self.name result["abbreviation"] = self.abbreviation // …and Int into NSNumber here. result["population"] = self.population return result } } let california = State(name: "California", abbreviation: "CA", population: 39_000_000) NSNotification(name: "foo", object: nil, userInfo: california.asPropertyList)
或者,你可能使用 Cocoa 容器类型,比如 NSDictionary :
// Swift 2 struct State { var name: String var abbreviation: String var population: Int var asPropertyList: NSDictionary { var result = NSMutableDictionary() // Implicit conversions turn String into NSString here… result["name"] = self.name result["abbreviation"] = self.abbreviation // …and Int into NSNumber here. result["population"] = self.population return result.copy() } } let california = State(name: "California", abbreviation: "CA", population: 39_000_000) // NSDictionary then implicitly converts to [NSObject: AnyObject] here. NSNotification(name: "foo", object: nil, userInfo: california.asPropertyList)
在 Swift 3 中,隐式转换不再存在了,所以上边的代码都不会正常运行了。迁移工具可能会建议使用 as 单独地转换每一个值来保证这个代码正常运行,但这里有更好的解决办法。Swift 现在引入 Cocoa API 作为 Any 和(或) AnyHashable 的可接受集合,所以我们可以改变集合的类型来使用 [AnyHashable: Any] 代替 [NSObject: AnyObject] 或者 NSDictionary ,而不需要改变其他任何代码:
// Swift 3 struct State { var name: String var abbreviation: String var population: Int // Change the dictionary type to [AnyHashable: Any] here... var asPropertyList: [AnyHashable: Any] { var result: [AnyHashable: Any] = [:] // No implicit conversions necessary, since String and Int are subtypes // of Any and AnyHashable result["name"] = self.name result["abbreviation"] = self.abbreviation result["population"] = self.population return result } } let california = State(name: "California", abbreviation: "CA", population: 39_000_000) // ...and you can still use it with Cocoa API here Notification(name: "foo", object: nil, userInfo: california.asPropertyList)
AnyHashable 类型
Swift 的 Any 类型可以处理任何类型,但 Dictionary 和 Set 需要 Hashable 的键,所以 Any 太通用而不能使用。从 Swift 3 开始,Swift 标准库提供一个新的 AnyHashable 类型,与 Any 类似,它作为所有的 Hashable 类型的父类,所以 String 、 Int 以及其他可哈希的类型可以被隐式地作为 AnyHashable 值,并且在 AnyHashable 之下的类型可以使用 is 、 as! 、 as? 搭台转换符号来动态地检查。 AnyHashable 会在从 Objective-C 引入无类型 NSDictionary 或者 NSSet 对象时使用,但它同样在纯 Swift 代码中作为创建牛逼合集或字典时好用。
未桥接上下文的显式转换
在确定的限制条件下,Swift 不能自动地桥接 C 和 Objective-C 构造。比如说,一些 C 和 Cocoa API 使用 id * 指针作为“输出”或者“输入输出”形式参数,并且自从 Swift 无法静态定义指针是如何使用的,它不能在内存中自动地执行桥接转换。万一如此,指针仍旧会显示为 UnsafePointer<AnyObject> 。如果你需要操作这些未桥接的 API,你可以显式地桥接转换,在你的代码中显式地使用 as Type 或者 as AnyObject 。
// ObjC @interface Foo - (void)updateString:(NSString **)string; - (void)updateObject:(id *)obj; @end
// Swift func interactWith(foo: Foo) -> (String, Any) { var string = "string" as NSString // explicit conversion foo.updateString(&string) // parameter imports as UnsafeMutablePointer<NSString> let finishedString = string as String var object = "string" as AnyObject foo.updateObject(&object) // parameter imports as UnsafeMutablePointer<AnyObject> let finishedObject = object as Any return (finishedString, finishedObject) }
额外的,Objective-C 协议在 Swift 中仍然是类限制的,所以你不能让 Swift 结构体或者枚举直接遵循 Objective-C 协议或者通过轻量级的泛型类型使用它们。你需要显式地转换 String as NSString , Array as NSArray 等等,来操作这些协议和 API。
AnyObject 成员查找
Any 没有与 AnyObject 一样的查找魔法方法。这可能会打破某些 Swift 2 中查找属性或者发送信息给无类型 Objective-C 对象。比如说,这个 Swift 2 代码:
// Swift 2 func foo(x: NSArray) { // Invokes -description by magic AnyObject lookup print(x[0].description) }
将会报错 description 不是 Swift 3 中 Any 的成员。你可以用 x[0] as AnyObject 转换来动态取回之前的行为:
// Swift 3 func foo(x: NSArray) { // Result of subscript is now Any, needs to be coerced to get method lookup print((x[0] as AnyObject).description) }
或者,强制转换值为你需要的具体对象类型:
func foo(x: NSArray) { // Cast to the concrete object type you expect print((x[0] as! NSObject).description) }
Objective-C 中 Swift 值类型
Any 可以处理任何结构体、枚举、元组或者其他你在 Swift 语言中可以定义的类型。Swift 3 中的 Objective-C 桥接可以相反表示任何 Swift 值作为兼容 id 的对象到 Objective-C。这能让 Cocoa 容器、 userInfo 字典以及其他对象中储存自定义 Swift 值类型更加简单。比如说,在 Swift 2 中,你可能要么在类中改变数据类型,要么手动包装它们,好把它们放进 NSSotification :
// Swift 2 struct CreditCard { number: UInt64, expiration: NSDate } let PaymentMade = "PaymentMade" // We can't attach CreditCard directly to the notification, since it // isn't a class, and doesn't bridge. // Wrap it in a Box class. class Box<T> { let value: T init(value: T) { self.value = value } } let paymentNotification = NSNotification(name: PaymentMade, object: Box(value: CreditCard(number: 1234_0000_0000_0000, expiration: NSDate())))
在 Swift 3 中,我们可以不用包装,直接把对象放进通知:
// Swift 3 let PaymentMade = Notification.Name("PaymentMade") // We can associate the CreditCard value directly with the Notification let paymentNotification = Notification(name: PaymentMade, object: CreditCard(number: 1234_0000_0000_0000, expiration: Date()))
在 Objective-C 中, CreditCard 值将显示为 id 兼容, NSObject 遵循的对象,实现了 isEqual: 、hash 以及 description ,要是在 Swift 中,它使用 Swift 的 Equatable 、 Hashable 和 CustomStringConvertible 实现。对 Swift 来说,值可以通过动态转换为其原本类型来取回:
// Swift 3 let paymentCard = paymentNotification.object as! CreditCard print(paymentCard.number) // 1234000000000000
注意,在 Swift 3.0 中,某些 Swift 和 Objective-C 通用结构类型将会桥接为不透明对象而不是习惯上的 Cocoa 对象。举例来说,无论是 Int 、 UInt 、 Double 和 Bool 桥接为 NSNumber ,其他有大小的数值类型比如说 Int8 、 UInt16 等等。Cocoa 结构体比如 CGRect 、 CGPoint 以及 CGSize 同样桥接为不透明对象,尽管大部分 Cocoa API 可以使用他们作为 NSValue 实例的包装。如果你看到比如 unrecognized selector sent to _SwiftValue 这样的错误,说明 Objective-C 代码正在尝试调用一个不透明 Swift 值类型中的方法,你可能需要手动包装这个值为 Objective-C 代码期望的类型实例。
一个特殊的需要注意的问题是可选项。Swift 中的 Any 可以处理所有内容,包括可选项,所以就有可能转一个可选项给 Objective-C API 而没有事先检查它,就算 API 声明为接收非空 id 。这通常就会搞出一个涉及 _SwiftValue 的运行时错误而不是一个编译时错误。包含在 Xcode 8.1 beta 中的 Swift 3.0.1 可以实现这些处理数字类型、Objective-C 结构体以及通过定位上述 NSNumber 、 NSValue 和可选绑定限制穿透可选项:
要避免向前的兼容问题,考虑到未来版本的 Swift 可能允许更多 Swift 类型桥接到符合语言习惯的 Objective-C 类型上,你不应该依赖 _SwiftValue 类不透明对象的实现细节。
Linux 的可移植性
Swift 程序依靠 Swift 核心库运行在 Linux 上,它是一个使用 Swift 写的原生 Foundation 版本,无需 Objective-C 运行时来桥接。 id 即 Any 允许核心库来直接使用原生 Any 和标准库值类型,同时保持了与苹果平台使用 Objective-C Foundation 实现的兼容。自从 Swift 不再在 Linux 上与 Objective-C 进行交互,也就不再支持桥接转换比如 string as NSString 或者 value as AnyObject 。想要在在 Cocoa 和 Swift 核心库之间移植 Swift 代码,就应当仅使用值类型。
了解更多
id 即 Any 是一个 Swift 语言受早期 Swift 版本用户反馈并从开源 Swift 演进进程中受到启发而提升的重要范例。如果你想要了解更多关于 id 即 Any 背后的动机和设计决定,原本的 Swift 演进提议在 GitHub 的 swift-evolution 仓库中可见:
- SE-0072: Fully eliminate implicit bridging conversions from Swift
- SE–0116: Import Objective-C id as Swift Any type
- SE–0131: Add AnyHashable to the standard library
最终的结果就是,Swift 是一门更加一致的语言,Cocoa API 由 Swift 的使用变得更加强大。