前言:在正式發布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點請隨手點贊;轉載請注明出處,謝謝支持。