Objective-C id as Swift Any

1,827 阅读9分钟

这是一片译文。原文链接在这儿~

Swift3以一种比以前的版本更强大的方式结合Objective-C的API。对于实例变量,Swift2把Objective-C中的id类型映射到 Swift中的AnyObject类型,通常它只能持有class类型。Swift2也为一些桥接值类型提供了隐式转换到AnyObject类型,例如:String, Array, Dictionary, Set 和一些数字,作为一种便利让Swift本地类型能够轻松的用于那些需要NSStringNSArray或者其他来自于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类型的元素。对于散列容器,比如DictionarySet,有一个新类型AnyHashable,可以持有一个任何遵守Swift 中Hashable协议的类型的值。总的来说,从Swift2到Swift3发生了下面这些类型映射改变:

Objective-CSwift 2Swift 3
idAnyObjectAny
NSArray *[AnyObject][Any]
NSDictionary *[NSObject: AnyObject][AnyHashable: Any]
NSSet *Set<NSObject>Set<AnyHashable>

许多情况下,为了响应这个改变,你的代码不需要明显地改变。Swift2依赖值类型隐式转换成AnyObject的代码在Swift3里仍然好使,只需要作为Any来传递。然而,有些地方你不得不改变变量和方法的声明类型,从而获得最好的Swift3编码体验。并且如果你的代码显式地使用AnyObject或者Cocoa类,例如NSString, NSArray, 或者 NSDictionary,你将会需要显式地使用as NSString 或者as String引入更多的角色,因为在Swift3里,对象类型和值类型的隐式转换不再被允许。

重写方法和遵守协议

当继承一个Objective-C类并且重写它的方法,或者遵守一个Objective-C协议,当父类方法使用了id时,这些方法的类型签名需要被更新。一些普通的例子比如:NSObjectisEqual:方法,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类型能够持有任何类型,但是DictionarySet要求键是Hashable的,所以Any太宽泛了。从Swift3开始,Swift标准库提供了一个新类型AnyHashable。和Any一样,它作为所有Hashable类型的父类型,因此StringInt和其他可散列类型的值可以被隐式的作为AnyHashable的值,并且一个AnyHashable内部的类型可以用is, as!, or as?这些动态类型装换操作符动态地检查。AnyHashable被用于需要从Object-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协议或者用轻便的泛型类使用它们。你需要用它们的协议和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<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())))

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类型。