【譯】:Objective-C id 和 Swift Any

前言:在正式發布Swift3.0版本后,蘋果官方于2016年10月12日更新了一篇關于講解Objective-C的id和Swift中Any類型的博客:Objective-C id as Swift Any。旨在指導開發者正確認識和使用idAny關鍵字。如果有同樣和我對兩者的使用不是很清楚或者正在著手將項目代碼從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提供了對一些橋接值類型(例如StringArrayDictionarySet和一些數字)的隱式轉換,以方便我們可以很容易的像使用NSStringNSArray或其他基礎的集合類一樣使用Swift中的原生類型。這些轉換與語言的其他部分不一致,使得很難理解什么可以用作AnyObject,導致會出現錯誤。

在Swift 3中,Objective-C中的id類型現在映射成了Swift中的Any類型,它可以代表任何類型的值,無論是類、枚舉、結構體還是任何其他Swift類型。 這種變化使得Swift中的Objective-C API更加靈活,因為Swift定義的值類型可以傳遞給Objective-C API并作為Swift中的類型獲取,從而無需手動“框選”類型(本人理解為轉換、解包)。 這些好處還擴展到集合類:Objective-C中的集合類型NSArrayNSDictionaryNSSet,以前只接受AnyObject類型的元素,現在可以保存任何類型的元素。 對于Swift中哈希類的集合,例如DictionarySet,有一個新類型AnyHashable可以保存任何遵守Swift中Hashable協議的類型的值。 總之,從Swift 2到Swift 3一些類型的映射關系變化如下圖:

圖片摘自蘋果官方博客

通常情況下,你的代碼不需要為適應這種變化做出大量的修改。 Swift 2中的代碼存在的AnyObject的值類型可以借助隱式轉換變成Any繼續在Swift 3中工作。 但是,有些地方你需要更改聲明的變量和方法類型才能獲得在Swift 3的編譯通過。另外,如果你的代碼顯式使用AnyObject或Cocoa中的類,如NSStringNSArrayNSDictionary,你將需要引入更多的顯式轉換使用作為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中處理這類數據需要用到AnyObjectNSObject來構建ArrayDictionarySet,并且依靠隱式橋接轉換來處理值的類型:

// 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接受AnyAnyHashable類型的集合,所以我們可以用[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表示的太廣泛。 從Swift 3開始,Swift標準庫提供了一個新的類型AnyHashable。 與Any類似,它充當所有Hashable類型的父類,因此StringInt和其他hashable類型的值都可以隱式地用作AnyHashable值,AnyHashable類型的值可以使用isas !動態檢查或者使用as?動態轉換運算符。 當從Objective-C導入無類型的NSDictionaryNSSet對象時可以使用AnyHashable,當然在純Swift中構建異構集合或字典時AnyHashable也很有用。

未鏈接上下文的顯式轉換

在某些確定的情況下,Swift不能自動橋接C和Objective-C。 例如,一些C和Cocoa API使用id *指針作為“out”“in-out”參數,并且由于Swift不能靜態地確定指針的使用方式,因此它不能對內存中的值自動執行橋接轉換 。 在這種情況下,指針仍將顯示為UnsafePointer <AnyObject>。 如果您需要使用到這些不能自動橋接轉換的API,您可以使用顯式橋接轉換,在代碼中使用as Typeas 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 NSStringArray 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 AnyObjectx[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值將顯示為一個兼容idNSObject對象(這里有疑問),使用Swift的EquatableHashableCustomStringConvertible如果存在原始的Swift類型,它將實現isEqual:hash和描述。 在Swift中,可以通過將值動態地轉換回其原始類型來檢索該值:

// Swift 3
let paymentCard = paymentNotification.object as! CreditCard
print(paymentCard.number) // 1234000000000000

請注意,在Swift 3.0中,一些常見的Swift和Objective-C結構類型將橋接為不透明對象(不透明對象什么鬼?),而不是慣用的Cocoa中的對象。例如IntUIntDoubleBool橋接到NSNumber,其他大小的數字類型,例如Int8UInt16等只橋接為不透明對象。可變結構如CGRectCGPointCGSize也作為不透明對象橋接,即使大多數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中的結構體和可選類型做了數字類型處理,以解決NSNumberNSValue和可選橋接中的上述限制:

* 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點請隨手點贊;轉載請注明出處,謝謝支持。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容