Swift3 接口和Objective-C 的APIs比之前的版本更强大。例如,Swift 2将Objective-C中的id类型映射到Swift中的AnyObject类型,通常只能保存值类型。 Swift 2还为AnyObject提供了对一些桥接值类型(例如String,Array,Dictionary,Set和一些数字)的隐式转换,以便原生Swift类型可以很容易地使用Cocoa APIs,如NSString,NSArray或Foundation的其他容器类。这些转换与语言的其他部分不一致,使得很难理解什么可以用作AnyObject,结果是bugs。
在Swift 3中,Objective-C中的id类型现在映射到Swift中的Any类型,它描述了任何类型的值,无论是类,枚举,结构还是任何其他Swift类型。这种变化使得Swift中的Objective-C APIs更加灵活,因为Swift定义的值类型可以传递给Objective-C API,并作为Swift类型返回,从而无需手动“box”类型。这些优点也扩展到集合:Objective-C集合类型NSArray,NSDictionary和NSSet,以前只接受AnyObject的元素,现在可以保存Any类型的元素。对于散列容器,例如Dictionary和Set,有一个新类型AnyHashable可以容纳任何类型的值遵循Swift Hashable协议。总之,从Swift 2到Swift 3以下类型映射更改为:
Objective-C | Swift2 | Swift3 |
---|---|---|
id | AnyObject | Any |
NSArray * | [AnyObject] | [Any] |
NSDictionary * | [NSObject: AnyObject] | [AnyHashable: Any] |
NSSet * | Set |
Set |
在许多情况下,您的代码不必为响应此更改而显着地更改。 Swift 2中的代码依赖于隐式转换为AnyObject的值类型,它将继续按照Any的原样在Swift 3中工作。但是,有些地方需要更改声明的变量和方法类型,并获得Swift 3的最佳体验。另外,如果你的代码显式使用AnyObject或Cocoa类,如NSString,NSArray或NSDictionary,你将需要引入更多的显式转换使用as NSString或as String,因为在Swift 3中对象和值类型之间的隐式转换不再允许。Xcode中的自动迁移器将进行最小的更改,以保持您的代码从Swift 2到3编译,但结果可能不总是最优雅的。本文将介绍您可能需要做的一些更改,以及在更改代码以充分利用id为Any时需要注意的一些陷阱。
覆盖方法和遵循协议
当子类化一个Objective-C类并覆盖它的方法,或者遵循一个Objective-C协议,当父方法在Objective-C中使用id时,需要更新方法的类型。一些常见的例子是NSObject类的isEqual:方法和NSCopying协议的copyWithZone:方法。在Swift 2中,你将写一个遵循NSCopying的NSObject子类,如下所示:
|
|
在Swift 3中,除了将命名从copyWithZone(_ :)更改为copy(with :)之外,还需要将这些方法的签名更改为Any而不是AnyObject:
|
|
非类型集合
属性列表,JSON和用户信息字典在Cocoa中很常见,Cocoa本地将这些表示为非类型化集合。在Swift 2中,为此需要构建Array,Dictionary或Set with AnyObject或NSObject元素,依靠隐式桥接转换来处理值类型:
|
|
或者,您可以使用Cocoa容器类,例如NSDictionary:
|
|
在Swift 3中,隐式转换已经消失,因此上述两个片段都不会按原样工作。迁移者可能建议用as单独转换每个值,以保持此代码的工作,但有一个更好的解决方案。 Swift现在导入Cocoa API接受Any和/或AnyHashable的集合,所以我们可以更改集合类型为[AnyHashable:Any]替换[NSObject:AnyObject]或NSDictionary,而不更改任何其他代码:
|
|
AnyHashable类型
Swift的Any类型可以保存任何类型,但是Dictionary和Set需要的键是Hashable,所以Any是太普通。从Swift 3开始,Swift标准库提供了一个新的类型AnyHashable。与Any类似,它充当所有Hashable类型的父类型,因此String,Int和其他hashable类型的值可以隐式地用AnyHashable值,AnyHashable中的类型可以使用is,as!或as?动态检查,动态转换运算符。当从Objective-C导入无类型的NSDictionary或NSSet对象时,使用AnyHashable,但是在纯Swift中也可用于构建异构集合或字典。
未链接上下文的显式转换
在某些有限的情况下,Swift不能自动桥接C和Objective-C构造。例如,一些C和Cocoa API使用id * 指针作为“out”或“in-out”参数,并且由于Swift不能静态地确定指针的使用方式,因此它不能对内存中的值自动执行桥接转换。在这种情况下,指针仍将显示为UnsafePointer
|
|
|
|
另外,Objective-C协议在Swift中仍然是类约束,所以你不能让Swift结构或枚举直接遵循Objective-C协议或者使用轻量级通用类。您需要使用这些协议和API显式转换String as NSString,Array as NSArray等。
AnyObject成员查找
Any没有与AnyObject相同的魔法方法查找行为。这可能会破坏一些Swift 2代码,查找属性或发送消息到一个无类型的Objective-C对象。例如,这个Swift 2的代码:
|
|
将抱怨description不是Swift 3中Any的成员。可以将x [0] as AnyObject以获取动态行为:
|
|
或者,将值强制转换为您期望的具体对象类型:
|
|
Swift中的值类型
Any可以hold住任何结构,枚举,元组或你可以在语言中定义的其他Swift类型。 Swift 3中的Objective-C桥接器可以提供任何Swift值作为Objective-C的id兼容对象。这使得更容易在Cocoa容器,userInfo字典和其他对象中存储自定义Swift值类型。例如,在Swift 2中,您需要将数据类型更改为类,或者手动box它们,以将它们的值附加到NSNotification:
|
|
使用Swift 3,我们可以取消box,并将对象直接附加到Notification:
|
|
在Objective-C中,CreditCard值将作为一个id兼容的,继承NSObject对象实现isEqual:,hash和description,如果它们存在原生的Swift类型的话,使用Swift的Equatable,Hashable和CustomStringConvertible。从Swift中,可以通过将其动态地转换回其原始类型来检索该值:
|
|
请注意,在Swift 3.0中,一些常见的Swift和Objective-C结构类型将桥接为不透明对象,而不是惯用的Cocoa对象。例如,Int,UInt,Double和Bool桥接到NSNumber,其他大小的数字类型,例如Int8,UInt16等只桥接为不透明对象。可变结构如CGRect,CGPoint和CGSize也作为不透明对象桥接,即使大多数Cocoa API期望它们一起作为对象在NSValue实例中被box。如果你看到unrecognized selector sent to _SwiftValue错误,这表明Objective-C代码试图调用一个不透明的Swift值类型的方法,你可能需要手动box该类的实例中Objective-C代码期望的值。
要注意的一个特殊问题是Optionals。 Swift Any可以代表任何东西,包括一个Optional,所以可以在不首先检查它情况下将一个包装的Optional传递给Objective-C API,即使API被声明为一个nonnull id类型。这通常表现为包括_SwiftValue的运行时错误,而不是编译时错误。 Xcode 8.1 beta中包含的Swift 3.0.1通过实现这些建议来透明地处理数字类型,Objective-C结构和可选类型,以解决NSNumber,NSValue和Optional桥接中的上述限制:
- SE–0139: Bridge Numeric Types to NSNumber and Cocoa Structs to NSValue
- SE–0140: Warn when Optional converts to Any, and bridge Optional As Its Payload Or NSNull
为了避免向前兼容性问题,你不应该依赖_SwiftValue类的不透明对象的实现细节,因为未来版本的Swift可能允许更多的Swift类型桥接到惯用的Objective-C类。
Linux可移植性
在Linux上使用Swift Core 运行的Swift程序库使用了原生Swift中编写的一个Foundation版本,没有Objective-C运行时桥接。 id-as-Any允许Core Libraries直接使用原生Swift Any和标准库值类型,同时使用Objective-C Foundation实现保持与Apple平台上的代码兼容。由于Swift在Linux上不与Objective-C交互操作,因此不支持桥接转换,例如string as NSString或value as AnyObject。希望在Cocoa和Swift Core Libraries之间移植的Swift代码应该只使用值类型。
学习更多
id-as-Any是Swift语言改进的一个很好的例子,受到用户对早期版本的Swift的反馈的启发,并通过来自开放的Swift Evolution过程的回顾完善。如果你想更多地了解id-as-Any背后的动机和设计决策,原始的Swift Evolution提议可以在GitHub的swift-evolution仓库中找到:
最终结果是,Swift是一种更加一致的语言,当使用Swift时,Cocoa API变得更强大。