前言:在正式发布Swift3.0版本后,苹果官方于2016年10月12日更新了一篇关于讲解Objective-C的id
和Swift中Any
类型的博客:Objective-C id as Swift Any。旨在指导开发者正确认识和使用id
和Any
关键字。如果有同样和我对两者的使用不是很清楚或者正在着手将项目代码从Swift2.0迁移到Swift3.0版本的小伙伴可以们阅读一下由本人翻译的译文。以下是译文全部内容:
2016年10月12日
Objective-C 中的 id 和 Swift 中的 Any
在使用Objective-C API方面Swift 3的接口表现的比之前版本更强大。例如Swift 2将Objective-C中的id
类型映射成Swift中的AnyObject
类型,通常只能保存类这种类型的值。 Swift 2还为AnyObject
提供了对一些桥接值类型(例如String
,Array
,Dictionary
,Set
和一些数字)的隐式转换,以方便我们可以很容易的像使用NSString
,NSArray
或其他基础的集合类一样使用Swift中的原生类型。这些转换与语言的其他部分不一致,使得很难理解什么可以用作AnyObject
,导致会出现错误。
在Swift 3中,Objective-C中的id
类型现在映射成了Swift中的Any
类型,它可以代表任何类型的值,无论是类、枚举、结构体还是任何其他Swift类型。 这种变化使得Swift中的Objective-C API更加灵活,因为Swift定义的值类型可以传递给Objective-C API并作为Swift中的类型获取,从而无需手动“框选”类型(本人理解为转换、解包)。 这些好处还扩展到集合类:Objective-C中的集合类型NSArray
、NSDictionary
和NSSet
,以前只接受AnyObject
类型的元素,现在可以保存任何类型的元素。 对于Swift中哈希类的集合,例如Dictionary
和Set
,有一个新类型AnyHashable
可以保存任何遵守Swift中Hashable
协议的类型的值。 总之,从Swift 2到Swift 3一些类型的映射关系变化如下图:
通常情况下,你的代码不需要为适应这种变化做出大量的修改。 Swift 2中的代码存在的AnyObject
的值类型可以借助隐式转换变成Any
继续在Swift 3中工作。 但是,有些地方你需要更改声明的变量和方法类型才能获得在Swift 3的编译通过。另外,如果你的代码显式使用AnyObject
或Cocoa中的类,如NSString
、NSArray
或NSDictionary
,你将需要引入更多的显式转换使用作为NSString
或作为字符串,因为对象和值类型之间的隐式转换在Swift 3中是不允许的。Xcode中的自动迁移器将进行最小的更改,以保证你的代码从Swift 2 到3能够编译成功,但情况并不总是有利的。 本文将指出你可能需要做的一些更改,以及在更改代码时将id
变成Any
使用需要注意的一些问题。
重写方法和遵守协议
新建一个继承自Objective-C类的子类并且重写它的方法,或者是遵守一个Objective-C中的协议,当父类的方法中使用了Objective-C中id
类型时,子类方法的类型此时应该被修改。一个常见的例子是NSObject
类的isEqual:
方法和NSCopying
协议的copyWithZone:
方法。在Swift 2中,你可以像下面一样新建一个遵守NSCopying
协议并继承自NSObject
的子类:
// Swift 2
class Foo: NSObject, NSCopying {
override func isEqual(_ x: AnyObject?) -> Bool { ... }
func copyWithZone(_ zone: NSZone?) -> AnyObject { ... }
}
在Swift 3中,除了将方法的命名从copyWithZone(_ :)
更改为copy(with :)
之外,你还需要将这些方法接受参数的类型从AnyObject
改为Any
。如下所示:
// 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中,隐式转换已经不支持,因此上述两段代码都不会按原样工作。 Xcode中的迁移器可能会建议你使用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
和其他hashable类型的值都可以隐式地用作AnyHashable
值,AnyHashable
类型的值可以使用is
、as !
动态检查或者使用as?
动态转换运算符。 当从Objective-C导入无类型的NSDictionary
或NSSet
对象时可以使用AnyHashable
,当然在纯Swift中构建异构集合或字典时AnyHashable
也很有用。
未链接上下文的显式转换
在某些确定的情况下,Swift不能自动桥接C和Objective-C。 例如,一些C和Cocoa API使用id *
指针作为“out”
或“in-out”
参数,并且由于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中的协议或者是使用轻量级的泛型类。 当您需要使用到这些协议和API时应该像这样String as NSString
、Array as NSArray
进行显式转换。
AnyObject属性查找
Any
没有与AnyObject
相同的返回对象的描述信息的方法。在Swift 2中Any
类型的对象查找属性或者是给一个无类型的Objective-C对象发送消息可能会导致奔溃。 例如下面这个使用Swift 2语法的代码:
// Swift 2
func foo(x: NSArray) {
// Invokes -description by magic AnyObject lookup
print(x[0].description)
}
Swift 3中description
不再是的Any
类型的对象的方法(通常我们重写这个方法以获得关于对象的一些描述信息)。你可以这样做x[0] as AnyObject
将x[0]
的值转换成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类型。在Swift3中Objective-C的桥接可以将任何Swift中的类型的值转换成Objective-C的id
类型的兼容的对象。 这使得更容易在Cocoa集合中存储userInfo
、字典和其他自定义Swift类型的对象。 例如,在Swift 2中,您需要将数据类型更改为类,或者手动加载它们,以将它们的值附加到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<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,我们不需要Box
这个类,可以将对象直接附加到通知中:
// 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
对象(这里有疑问),使用Swift的Equatable
,Hashable
和CustomStringConvertible
如果存在原始的Swift类型,它将实现isEqual:
、hash
和描述。 在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
等只桥接为不透明对象。可变结构如CGRect
、CGPoint
和CGSize
也作为不透明对象桥接,即使大多数Cocoa API使用的是NSValue
包装的实例。如果你看到一些类似unrecognized selector sent to _SwiftValue
的错误,这表明Objective-C代码试图调用一个不透明的Swift值类型的方法,你可能需要手动包装该类的实例而不是使用Objective-C转换的类型实例。
还有一个特殊问题是Optionals
。 Swift中的 Any
可以保存任何东西,包括一个Optional
,所以可以将一个可选类型的对象传递给Objective-C API,而不是首先检查它,即使API被声明为一个非空的id
,很可能会造成涉及_SwiftValue
的运行时错误,而不是编译时错误。 Xcode 8.1 beta中包含的Swift 3.0.1对Objective-C中的结构体和可选类型做了数字类型处理,以解决NSNumber
、NSValue
和可选桥接中的上述限制:
* 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
(以上两篇是Github上swift-evolution中讲解Swift类型转换的文章,有兴趣的同学可以看看)
为了避免向前兼容性问题,你不应该依赖_SwiftValue
类的不透明对象的实现,因为未来版本的Swift可能允许更多的Swift类型桥接到惯用的Objective-C类。
Linux可移植性
在Linux上使用Swift Core Swift运行的Swift程序库使用一个Swift本地编写的Foundation版本,没有Objective-C运行时桥接。id
映射成Any
允许Core Libraries直接使用本地Swift Any
和标准库值类型,同时使用Objective-C Foundation实现保持与Apple平台上的代码兼容。由于Swift在Linux上不与Objective-C交互操作,因此不支持桥接转换,例如字符串为NSString
或值为AnyObject
。希望在Cocoa和Swift Core Libraries之间移植的Swift代码应该只使用值类型。
学习更多
id
映射成Any
是Swift语言改进的一个很好的例子,受到用户对早期版本的Swift的反馈的启发,并通过来自开放的Swift Evolution过程的回顾完善。如果你想更多地了解id
映射Any
背后的动机和设计决策,原始的Swift Evolution提议可以在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是一种更加一致的语言,当使用Swift时,Cocoa API变得更强大。
< 所有博客文章
以上就是苹果官方Objective-C id as Swift Any博客的全部内容,由于本人理解有限以及时间仓促导致译文中难免存在瑕疵,如果大家有发现欢迎在评论区留言指出,本人将在第一时间修改过来;喜欢我的文章,可以关注我以此促进交流学习; 如果觉得此文戳中了你的G点请随手点赞;转载请注明出处,谢谢支持。