这是一片译文。原文链接在这儿~
Swift3以一种比以前的版本更强大的方式结合Objective-C的API。对于实例变量,Swift2把Objective-C中的id
类型映射到 Swift中的AnyObject
类型,通常它只能持有class类型。Swift2也为一些桥接值类型提供了隐式转换到AnyObject
类型,例如:String
, Array
, Dictionary
, Set
和一些数字,作为一种便利让Swift本地类型能够轻松的用于那些需要NSString
,NSArray
或者其他来自于Foundation
的容器类的Cocoa API。这些转换和Swift语言的剩余部分是不一致的,这让理解到底什么能被用作AnyObject
变的困难,导致出错。
在Swift3里,id
类型被映射成Any
类型,它描述了一个任何类型的值,无论是类,枚举,结构体,还是任何其他Swift类型。这个改变让Objective-C里API在Swift中更灵活,因为Swift定义的值类型可以被传递给Objective-C的API,并且作为Swift类型被取出来,消除了手动封包的需要。这些好处也扩展到了集合:Objective-C集合类型NSArray
, NSDictionary
, 和 NSSet,
之前只接受AnyObject
类型的元素,现在可以持有Any
类型的元素。对于散列容器,比如Dictionary
和Set
,有一个新类型AnyHashable
,可以持有一个任何遵守Swift 中Hashable
协议的类型的值。总的来说,从Swift2到Swift3发生了下面这些类型映射改变:
Objective-C | Swift 2 | Swift 3 |
---|---|---|
id | AnyObject | Any |
NSArray * | [AnyObject] | [Any] |
NSDictionary * | [NSObject: AnyObject] | [AnyHashable: Any] |
NSSet * | Set |
Set |
许多情况下,为了响应这个改变,你的代码不需要明显地改变。Swift2依赖值类型隐式转换成AnyObject
的代码在Swift3里仍然好使,只需要作为Any
来传递。然而,有些地方你不得不改变变量和方法的声明类型,从而获得最好的Swift3编码体验。并且如果你的代码显式地使用AnyObject
或者Cocoa类,例如NSString, NSArray,
或者 NSDictionary,
你将会需要显式地使用as NSString
或者as String
引入更多的角色,因为在Swift3里,对象类型和值类型的隐式转换不再被允许。
重写方法和遵守协议
当继承一个Objective-C类并且重写它的方法,或者遵守一个Objective-C协议,当父类方法使用了id
时,这些方法的类型签名需要被更新。一些普通的例子比如:NSObject
的isEqual:
方法,NSCopying
协议的copyWithZone:
方法。Swift2里,你会写一个像这样的继承NSObject
并遵守NSCopying
协议的子类:
// Swift 2
class Foo: NSObject, NSCopying {
override func isEqual(_ x: AnyObject?) -> Bool { ... }
func copyWithZone(_ zone: NSZone?) -> AnyObject { ... }
}
复制代码
Swift3里,除了把方法名从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原生把这些看做无类型的集合。出于这个目的,在Swift2里,用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)
复制代码
Swift3里面,隐式转换不存在了,因此以上的代码片段都不能正常工作。迁移器可能建议给每个转换单独添加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
太宽泛了。从Swift3开始,Swift标准库提供了一个新类型AnyHashable
。和Any
一样,它作为所有Hashable
类型的父类型,因此String
,Int
和其他可散列类型的值可以被隐式的作为AnyHashable
的值,并且一个AnyHashable
内部的类型可以用is, as!, or as?
这些动态类型装换操作符动态地检查。AnyHashable
被用于需要从Object-C中引入无类型的NSDictionary
或者 NSSet
对象,但是在纯Swift里创建复杂的集合和字典也有用。
针对未桥接的上下文进行显示转换
在特定的受限制的情况下,Swift不能自动桥接C和Objective-C结构。例如,一些C和Cocoa的API使用id *
指针作为输入和输出参数,并且由于Swift不能够静态的决定这个指针怎么使用,它就不能在内存里自动对这个值上执行桥接转换。像这样的情况下,这个指针将仍然是UnsafePointer
。如果你需要使用这些未桥接的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
let finishedString = string as String
var object = "string" as AnyObject
foo.updateObject(&object) // parameter imports as UnsafeMutablePointer
let finishedObject = object as Any
return (finishedString, finishedObject)
}
复制代码
另外,Objective-C中的协议在Swift中依然是受类限制的,因此你不能让Swift结构体和枚举直接遵守Objective-C协议或者用轻便的泛型类使用它们。你需要用它们的协议和API明确地转换String as NSString
, Array as NSArray
等等。
AnyObject成员检查
Any没有像AnyObject那样的魔术方法查找行为。这可能会打断那些查找一个属性或者发送一个消息给一个无类型的Object-C对象的Swift2代码。例如,这段Swift2代码:
// Swift 2
func foo(x: NSArray) {
// Invokes -description by magic AnyObject lookup
print(x[0].description)
}
复制代码
在Swift3里将会抱怨description
不是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)
}
复制代码
Object-C中的Swift值类型
Any
能够持有任何结构体,枚举,元组,或者是你能够在这个语言里定义的其他类型。Swift3里的Object-C桥接可以反过来把任何Swift值呈现为id
-Object-C兼容对象。这使得在Cocoa容器、userInfo字典和其他对象里储存自定义的Swift值类型更加容易。例如,在Swift2里,你需要把你的数据类型改成类,或者手动封包它们,来把它们的值附加给一个NSNotification
:
// 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 {
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())))
复制代码
Swift3里,我们不需要封包,可以直接把这个对象附加给通知:
// 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()))
复制代码
在Object-C里,这个CreditCard
值将会是id
兼容的,NSObject--实现了isEqual:
, hash
, 和 description
的遵守对象,使用Swift的Equatable
, Hashable
, 和 CustomStringConvertible
实现,如果它们存在原始的Swift类型。Swift中,这个值可以通过动态转换回它原始的类型而被取回:
// Swift 3
let paymentCard = paymentNotification.object as! CreditCard
print(paymentCard.number) // 1234000000000000
复制代码
在Swift3.0中,你要清楚一些常见的Swift和Object-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代码期望的类的实例手动封包那个值。
一个需要特别注意的问题是可选值。Any
能够持有任何东西,包括一个可选值,因此把一个包装的可选值传给一个Objective-C API而不先检查它是可能的,即使这个API声明为需要nonnull id
。这通常会表现为一个包含_SwiftValue
的运行时错误,而不是编译期错误。Xcode8.1beta版里的Swift3.0.1通过定位上述的NSNumber
, NSValue
, 和 Optional
桥接限制实现这些提议,来显式地处理数字类型,Objective-C结构体和可选值。
为了避免传递兼容问题,你不应该依赖_SwiftValue
类的不透明对象的实现细节,因为将来的Swift版本可能允许更多的Swift类型桥接到符合语言习惯的Object-C类型。