[譯] Swift 中的動態特性

在本教程中,你將學習如何使用 Swift 中的動態特性編寫簡潔、清晰的代碼並快速解決沒法預料的問題。html

做爲一名忙碌的 Swift 開發人員,你的需求對你來講是特定的,但對全部人來講都是共同的。你但願編寫整潔的代碼,一目瞭然地瞭解代碼中的內容並快速解決沒法預料的問題。前端

本教程將 Swift 的動態性和靈活性結合在一塊兒來知足那些需求。經過使用最新的 Swift 技術,你將學習如何自定義輸出到控制檯,掛鉤第三方對象狀態更改,並使用一些甜蜜的語法糖來編寫更清晰的代碼。android

具體來講,你將學習如下內容:ios

  • Mirror
  • CustomDebugStringConvertible
  • 使用 keypath 進行鍵值監聽(KVO)
  • 動態查找成員
  • 相關技術

最重要的是,你將度過一段美好的時光!git

本教程須要 Swift 4.2 或更高版本。你必須下載最新的 Xcode 10 或安裝最新的 Swift 4.2程序員

此外,你必須瞭解基本的 Swift 類型。Swift 入門教程(原文連接)中的枚舉類和結構體是一個很好的起點。雖然不是嚴格要求,但你也能夠查看在 Swift 中實現自定義下標原文連接)。github

入門

在開始以前,請先下載資源(入門項目和最終項目)。數據庫

爲了讓你專一於學習 Swift 動態特性,其餘所需的全部代碼都已經爲你寫好了!就像和一隻友好的導盲犬一塊兒散步同樣,本教程將指導你完成入門代碼中的全部內容。編程

快樂的狗狗json

在名爲 DynamicFeaturesInSwift-Starter 的入門項目代碼目錄中,你將看到三個 Playground 頁面:DogMirrorDogCatcherKennelsKeyPath。Playground 在macOS上運行。本教程與平臺無關,僅側重於 Swift 語言。

使用 Mirror 的反射機制與調試輸出

不管你是斷點調試追蹤問題仍是隻探索正在運行的代碼,控制檯中的信息是否整潔都會產生比較大的影響。Swift 提供了許多自定義控制檯輸出和捕獲關鍵事件的方法。對於自定義輸出,它沒有 Mirror 深刻。Swift 提供比最強大的雪橇犬還要強大的力量,能把你從冰冷的雪地拉出來!

西伯利亞雪橇犬

在瞭解有關 Mirror 的更多信息以前,你首先要爲一個類型編寫一些自定義的控制檯輸出。這將有助於你更清楚地瞭解目前正在發生的事情。

CustomDebugStringConvertible

用 Xcode 打開 DynamicFeaturesInSwift.playground 並前往 DogMirror 頁面。

爲了記念那些迷路的可愛的小狗,它們被捕手抓住而後與它們的主人團聚,這個頁面有 Dog 類和 DogCatcherNet 類。首先咱們看一下 DogCatcherNet 類。

因爲丟失的小狗必須被捕獲並與其主人團聚,因此咱們必須支持捕狗者。你在如下項目中編寫的代碼將幫助捕狗者評估捕狗網的質量。

在 Playground 裏,看看如下內容:

enum CustomerReviewStars { case one, two, three, four, five }
複製代碼
class DogCatcherNet {
  let customerReviewStars: CustomerReviewStars
  let weightInPounds: Double
  // ☆ Add Optional called dog of type Dog here

  init(stars: CustomerReviewStars, weight: Double) {
    customerReviewStars = stars
    weightInPounds = weight
  }
}

複製代碼
let net = DogCatcherNet(stars: .two, weight: 2.6)
debugPrint("Printing a net: \(net)")
debugPrint("Printing a date: \(Date())")
print()

複製代碼

DogCatcherNet 有兩個屬性:customerReviewStarsweightInPounds。客戶評論的星星數量反映了客戶對淨產品的感覺。以磅爲單位的重量告訴狗捕捉者他們將經歷拖拽網的負擔。

運行 Playground。你應該看到的內容前兩行與下面相似:

"Printing a net: __lldb_expr_13.DogCatcherNet"
"Printing a date: 2018-06-19 22:11:29 +0000"
複製代碼

正如你所見,控制檯中的調試輸出會打印與網絡和日期相關的內容。保佑它吧!代碼的輸出看起來像是由機器寵物製做的。這隻寵物已經盡力了,但它須要咱們人類的幫助。正如您所看到的,它打印出了諸如 「__lldb_expr_」 之類的額外信息。打印出的日期能夠提供更有用的功能,可是這是否足以幫助你追蹤一直困擾着你的問題還尚不清楚。

爲了增長成功的機會,你須要用到 CustomDebugStringConvertible 的魔力來基礎自定義制臺輸出。在 Playground 上,在 **DogCatcherNet **裏的 ☆ Add Conformance to CustomDebugStringConvertible 下面添加如下代碼:

extension DogCatcherNet: CustomDebugStringConvertible {
  public var debugDescription: String {
    return "DogCatcherNet(Review Stars: \(customerReviewStars), Weight: \(weightInPounds)"
  }
}

複製代碼

對於像 DogCatcherNet 這樣的小東西,一個類能夠遵循 CustomDebugStringConvertible 並使用 debugDescription 屬性來提供本身的調試信息。

運行 Playground。除日期值會有差別外,前兩行應包括:

"Printing a net: DogCatcherNet(Review Stars: two, Weight: 2.6)"
"Printing a date: 2018-06-19 22:10:31 +0000"
複製代碼

對於具備許多屬性的較大類型,此方法須要顯式樣板的類型。對於有決心的人來講,這不是問題。若是時間不夠,還有其餘選項,例如 dump

Dump

如何避免須要手動添加樣板代碼?一種解決方案是使用 dumpdump 是一個通用函數,它打印出類型屬性的全部名稱和值。

Playground 已經包含 dump 出捕狗網和日期的調用。代碼以下所示:

dump(net)
print()

dump(Date())
print()
複製代碼

運行 playground。控制檯的輸出以下:

▿ DogCatcherNet(Review Stars: two, Weight: 2.6) #0
  - customerReviewStars: __lldb_expr_3.CustomerReviewStars.two
  - weightInPounds: 2.6

▿ 2018-06-26 17:35:46 +0000
  - timeIntervalSinceReferenceDate: 551727346.52924
複製代碼

因爲你目前使用 CustomDebugStringConvertible 完成的工做,DogCatcherNet 看起來比其餘方式更好。輸出包含:

DogCatcherNet(Review Stars: two, Weight: 2.6)
複製代碼

dump 還會自動輸出每一個屬性。棒極了!如今是時候使用 Swift 的 Mirror 讓這些屬性更具可讀性了。

Swift Mirror

魔鏡魔鏡,告訴我,誰纔是世界上最棒的狗?

Mirror 容許你在運行時經過 playground 或調試器顯示任何類型實例的值。簡而言之,Mirror 的強大在於內省。內省是反射 的一個子集。

建立一個 Mirror 驅動的狗狗日誌

是時候建立一個 Mirror 驅動的狗狗日誌了。爲了協助調試,最理想的是經過日誌功能向控制檯顯示捕狗網的值,其中自定義輸出帶有表情符號。日誌功能應該可以處理你傳遞的任何類型。

建立一個 Mirror

是時候建立一個使用 Mirror 的日誌功能了。首先,在 ☆ Create log function here 添加如下代碼:

func log(itemToMirror: Any) {
  let mirror = Mirror(reflecting: itemToMirror)
  debugPrint("Type: 🐶 \(type(of: itemToMirror)) 🐶 ")
}
複製代碼

這將爲傳入的對象建立鏡像,鏡像容許你迭代實例的各個部分。

將如下代碼添加到 log(itemToMirror:) 的末尾:

for case let (label?, value) in mirror.children {
  debugPrint("⭐ \(label): \(value) ⭐")
}
複製代碼

這將訪問鏡像的 children 屬性,獲取每一個標籤值對,而後將它們打印到控制檯。標籤值對的類型別名爲 Mirror.Child。對於 DogCatcherNet 實例,代碼迭代捕狗網對象的屬性。

澄清一點,被檢查實例的子級與父類或子類層次結構無關。經過鏡像訪問的孩子只是被檢查實例的一部分。

如今,是時候調用新的日誌方法了。在 ☆ Log out the net and a Date object here 添加如下代碼:

log(itemToMirror: net)
log(itemToMirror: Date())
複製代碼

運行 playground。你會在控制檯的底部看到一些很棒的輸出:

"Type: 🐶 DogCatcherNet 🐶 "
"⭐ customerReviewStars: two ⭐"
"⭐ weightInPounds: 2.6 ⭐"
"Type: 🐶 Date 🐶 "
"⭐ timeIntervalSinceReferenceDate: 551150080.774974 ⭐"
複製代碼

這顯示了全部屬性的名稱和值。名稱和你在代碼中寫的同樣。例如,customerReviewStars 其實是如何在代碼中拼寫屬性名稱。

CustomReflectable

若是你想要讓更多的狗或者小馬也能更清楚地顯示其中的屬性名稱應該怎麼辦呢?若是你又不想顯示某些屬性要怎麼辦呢?若是你但願在技術上顯示的不屬於該類型的每一項,又該怎麼辦呢?這時你可使用 CustomReflectable

CustomReflectable 提供了一個接口,你可使用自定義的 Mirror 來指定須要顯示類型實例的哪些部分。要遵循 CustomReflectable 協議,這個類必須定義 customMirror 屬性。

在與幾位捕手程序員交談後,你發現打印捕狗網的 weightInPounds 屬性並無幫助於調試。可是 customerReviewStars 的信息很是有用,他們但願customerReviewStars 的標籤顯示爲 「Customer Review Stars」。如今,是時候讓 DogCatcherNet 遵循 CustomReflectable 了。

☆ Add Conformance to CustomReflectable for DogCatcherNet here 後面添加如下代碼:

extension DogCatcherNet: CustomReflectable {
  public var customMirror: Mirror {
    return Mirror(DogCatcherNet.self,
                  children: ["Customer Review Stars": customerReviewStars,
                            ],
                  displayStyle: .class, ancestorRepresentation: .generated)
  }
}
複製代碼

運行 playground 能看到以下的輸出:

"Type: 🐶 DogCatcherNet 🐶 "
"⭐ Customer Review Stars: two ⭐"
複製代碼

狗狗上哪去了呢? 捕狗網的做用是當有狗來的時候抓住它。當網裏裝滿狗時,必須有辦法在網中提取有關狗的信息。具體來講,你須要狗的名字和年齡。

Playground 的頁面已經有一個 Dog 類。是時候將 DogDogCatcherNet 鏈接起來了。在標記了 ☆ Add Optional called dog of type Dog here 的標籤下爲 DogCatcherNet 添加如下屬性:

var dog: Dog?
複製代碼

隨着狗的屬性添加到了 DogCatcherNet,是時候再將狗添加到DogCatcherNetcustomMirror 了。在 children: ["Customer Review Stars": customerReviewStars, 這一行下添加如下的一個字典:

"dog": dog ?? "",
"Dog name": dog?.name ?? "No name"
複製代碼

這將使用其默認調試描述和狗的名稱輸出狗的屬性。

是時候輕輕地把狗放進網裏了。如今把 ☆ Uncomment assigning the dog 那一行取消註釋,可愛的小狗就能夠被放到網裏了。

net.dog = Dog() // ☆ Uncomment out assigning the dog
複製代碼

運行 Playground 能看到以下輸出:

"Type: 🐶 DogCatcherNet 🐶 "
"⭐ Customer Review Stars: two ⭐"
"⭐ dog: __lldb_expr_23.Dog ⭐"
"⭐ Dog name: Abby ⭐"
複製代碼

Mirror 的便利

可以看到一切真是太好了。可是,有些時候你只想看到鏡像的其中一部分。爲此,使用 descendant(_:_:) 來取出名稱和年齡:

let netMirror = Mirror(reflecting: net)

print ("The dog in the net is \(netMirror.descendant("dog", "name") ?? "nonexistent")")
print ("The age of the dog is \(netMirror.descendant("dog", "age") ?? "nonexistent")")
複製代碼

運行 Playground,你將在控制檯底部看到以下輸出:

The dog in the net is Bernie
The age of the dog is 2
複製代碼

那是煩人的動態內省。它對於調試自定義的類型很是有用!在深刻探討了 Mirror 後,你就完成了 DogMirror.xcplaygroundpage

封裝 Mirror 調試輸出

有不少方法能夠追蹤程序中發生了什麼,例如獵犬。CustomDebugStringConvertibledumpMirror 能讓你更清楚地看到你在尋找什麼。Swift 的內省功能很是有用,特別是當你開始構建更龐大更復雜的應用程序時!

KeyPath

有關跟蹤程序中發生的事情的狀況,Swift 有一些很棒的解決方案,叫作 keypath。要捕獲事件,例如當第三方庫對象中的值發生更改時,請向 鍵值監聽 尋求幫助。

在 Swift 中,keyPath 是強類型的路徑,其類型在編譯時被檢查。在 Objective-C 中,它們只是字符串。教程 Swift 4 新特性 在鍵值編碼部分的概念方面作得很好。

有幾種不一樣類型的 KeyPath。常見的類型包括 KeyPathWritableKeyPathReferenceWritableKeyPath。如下是它們的摘要:

  • KeyPath:指定特定值類型的根類型。
  • WritableKeyPath:可寫入的 KeyPath,它不能用於類。
  • ReferenceWritableKeyPath:用於類的可寫入 KeyPath,由於類是引用類型。

使用 KeyPath 的一個例子是在對象的值發生更改後觀察或捕獲。

當你遇到涉及第三方對象的 bug 時,知道該對象的狀態什麼時候發生變化就顯得尤其重要。除了調試以外,有時在第三方對象(例如 Apple 的 UIImageView 對象)中的值發生更改時,調用自定義代碼進行響應是有意義的。在 Design Patterns on iOS using Swift – Part 2/2 中,你能夠了解有關觀察者模式的更多信息。

然而,這裏有一個與狗窩相關的用例,它適合咱們的狗狗世界。若是沒有強大的鍵值監聽,捕狗者如何輕易地知道何時狗窩能夠放入更多的狗呢?雖然許多捕狗者只是喜歡把他們發現的每隻丟失的狗帶回家,但這是不切實際的。

所以,只想幫助狗回家的捕狗者須要知道何時狗窩能夠放入狗。實現這一目標的第一步是建立一個 KeyPath。打開 KennelsKeyPath 頁面,而後在 ☆Add KeyPath here 下面添加:

let keyPath = \Kennels.available
複製代碼

這就是你建立 KeyPath 的方法。你能夠在類型上使用反斜槓,後跟一系列點分隔的屬性,在這種狀況下能取到最後一個屬性。要使用 KeyPath 來監聽對 available 屬性的更改,請在 ☆ Add observe method call here 以後添加如下代碼:

kennels.observe(keyPath) { kennels, change in
  if kennels.available {
    print("kennels are available")
  }
}
複製代碼

點擊運行,你能看到控制檯的輸出以下:

Kennels are available.
複製代碼

這種方法對於肯定值什麼時候發生變化的狀況也頗有用。想象一下,咱們竟然可以調試第三方框架裏對象狀態的修改!當有意思的項發生變化時,能夠確保你不用看到煩人的錯誤調用的樹的輸出。

到如今爲止你已經完成了 KennelsKeyPath 項目!

理解動態成員查詢

若是你一直在緊跟 Swift 4.2 的變化,你可能據說過 動態成員查詢(Dynamic Member Lookup)。若是沒有,你在這裏不只僅只是學習這個概念。

在本教程的這一部分中,你將經過一個如何建立真正的 JSON DSL(域規範語言)的示例來看到 Swift 中 動態成員查詢 的強大功能,該示例容許調用者使用點表示法來訪問來自 JSON 數據的值。

動態成員查詢 使編碼人員可以對編譯時不存在的屬性使用點語法,而不是使用混亂的方式。簡而言之,你將擁有那些屬性運行時必存在的信念來編寫代碼,從而得到易於閱讀的代碼。

正如 proposal for this featureassociated conversations in the Swift community 中提到的,這個功能爲和其餘語言的互操做性提供了極大的支持,例如 Python,數據庫實現者和圍繞「基於字符串的」 API(如 CoreImage)建立無樣板包裝器等。

@dynamicMemberLookup 簡介

打開 DogCatcher 頁面並查看代碼。在 Playground 裏, 表示狗的運行有一個 方向

使用 dynamicMemberLookup 的功能,即便這些屬性沒有明確存在,也能夠訪問 directionOfMovementmoving。如今是時候讓 Dog 變的動態了。

把 dynamicMemberLookup 添加到 Dog

激活此動態功能的方法是使用註解 @dynamicMemberLookup

☆ Add subscript method that returns a Direction here 下添加如下代碼:

subscript(dynamicMember member: String) -> Direction {
  if member == "moving" || member == "directionOfMovement" {
    // Here's where you would call the motion detection library
    // that's in another programming language such as Python
    return randomDirection()
  }
  return .motionless
}
複製代碼

如今經過取消 ☆ Uncomment this line 下面的註釋,來將標記 dynamicMemberLookup 添加到 Dog 中。

你如今能夠訪問名爲 directionOfMovementmoving 的屬性。嘗試在 ☆ Use the dynamicMemberLookup feature for dynamicDog here 下面上添加如下內容:

let directionOfMove: Dog.Direction = dynamicDog.directionOfMovement
print("Dog's direction of movement is \(directionOfMove).")

let movingDirection: Dog.Direction = dynamicDog.moving
print("Dog is moving \(movingDirection).")
複製代碼

運行 Playground。因爲狗有時在 左邊 且有時在 右邊,所以你應該看到輸出的前兩行相似於:

Dog's direction of movement is left. Dog is moving left. 複製代碼

重載下標 (dynamicMember:)

Swift 支持用不一樣的返回值重載下標聲明。在 ☆ Add subscript method that returns an Int here 下面嘗試添加返回一個 Intsubscript

subscript(dynamicMember member: String) -> Int {
  if member == "speed" {
    // Here's where you would call the motion detection library
    // that's in another programming language such as Python.
    return 12
  }
  return 0
}
複製代碼

如今你能夠訪問名爲 speed 的屬性。經過在以前添加的 movingDirection 下添加如下內容來加快勝利速度:

let speed: Int = dynamicDog.speed
print("Dog's speed is \(speed).")
複製代碼

運行 Playground,輸出應該包含如下內容:

Dog's speed is 12. 複製代碼

是否是太棒了。即便你須要訪問其餘編程語言(如Python),這也是一個強大的功能,可使代碼保持良好狀態。如前所述,有一個問題...

「想抓我?」我全聽到了。

給狗編譯並完成代碼

爲了換取動態運行時的特性,你沒法得到依賴於 subscript(dynamicMember:) 功能屬性的編譯時檢查的好處。此外,Xcode 的代碼自動補全功能也沒法幫助你。但好消息是專業 iOS 開發者能閱讀到比他們編寫的還要多的代碼。

動態成員查詢 給你的語法糖只是扔掉了。這是一個很好的功能,使 Swift 的某些特定用例和語言互操做性可讓人看到而且使人愉快。

友好的捕狗者

動態成員查詢 的原始提案解決了語言互操做性問題,尤爲是對於 Python。可是,這並非惟一有用的狀況。

爲了演示純粹的 Swift 用例,你將使用 DogCatcher.xcplaygroundpage 中的 JSONDogCatcher 代碼。它是一個簡單的結構,具備一些屬性,用於處理StringInt 和 JSON 字典。使用這樣的結構,你能夠建立一個 JSONDogCatcher 並最終搜索特定的 StringInt 值。

傳統下標方法

實現相似遍歷 JSON 字典的傳統方法是使用 下標 方法。Playground 已經包含傳統的 下標 實現。使用 subscript 方法訪問 StringInt 值一般以下所示,而且也在 Playground 中:

let json: [String: Any] = ["name": "Rover", "speed": 12,
                          "owner": ["name": "Ms. Simpson", "age": 36]]

let catcher = JSONDogCatcher.init(dictionary: json)

let messyName: String = catcher["owner"]?["name"]?.value() ?? ""
print("Owner's name extracted in a less readable way is \(messyName).")
複製代碼

雖然你必須遍歷查詢括號,引號和問號來得到其中的數據,但這頗有效。 運行 Playground,你看到的輸出將會以下:

Owner's name extracted in a less readable way is Ms. Simpson. 複製代碼

雖然它能夠解決問題,可是使用點語法就能夠更輕鬆了。使用 動態成員查詢,你能夠深刻了解多級 JSON 數據結構。

將 dynamicMemberLookup 添加到 Dog Catcher 就像 Dog 同樣,是時候將 dynamicMemberLookup 屬性添加到 JSONDogCatcher 結構中了。

☆ Add subscript(dynamicMember:) method that returns a JSONDogCatcher here 下添加如下代碼:

subscript(dynamicMember member: String) -> JSONDogCatcher? {
  return self[member]
}
複製代碼

下標方法 subscript(dynamicMember:) 調用已存在的 下標 方法,但刪除了使用括號和 String 做爲鍵的樣板代碼。如今,取消在 JSONDogCatcher 上 標有 ☆ Uncomment this line 的註釋:

@dynamicMemberLookup
struct JSONDogCatcher {
複製代碼

有了這個以後,你就可使用點語法來得到狗的速度和它主人的名字。嘗試在 ☆ Use dot notation to get the owner’s name and speed through the catcher 下添加如下代碼:

let ownerName: String = catcher.owner?.name?.value() ?? ""
print("Owner's name is \(ownerName).")

let dogSpeed: Int = catcher.speed?.value() ?? 0
print("Dog's speed is \(dogSpeed).")
複製代碼

運行 Playground,你會看到控制檯輸出了速度和狗主人的名字:

Owner's name is Ms. Simpson. Dog's speed is 12.
複製代碼

如今你獲得了主人的名字,狗捕手能夠聯繫主人來讓他知道他的狗被找到了!

多麼幸福的結局!狗和它的主人再次團聚,並且代碼也看起來更整潔。經過 Swift 的動態的力量,這條活潑的狗能夠回到後院去追兔子了。

辛普森的狗喜歡追逐而不是追趕

後記

你可使用本教程頂部的 下載材料 連接下載到項目的完整版本。

在本教程中,你利用了 Swift 4.2 中提供的動態功能。瞭解了 Swift 的內省反射功能(例如 Mirror)自定義控制檯輸出,使用 KeyPath 進行 鍵值監聽動態成員查找

經過學習動態的功能,你能夠清楚地看到有用的信息,擁有更易讀的代碼,併爲你的應用程序,通用框架或者是庫提供一些強大的運行時功能。

深刻 Mirror 的官方文檔和相關項目進行探索是值得的。有關 **鍵值監聽 ** 的更多信息,請看使用 Swift 的 iOS 設計模式。想了解更多 Swift 4.2 新特性,請看 What’s New in Swift 4.2?

關於 Swift 4.2 裏 動態成員查找 功能,查看 Swift 提案 SE-0195: 「Introduce User-defined ‘Dynamic Member Lookup’ Types」,其中介紹了 dynamicMemberLookup 註解和潛在用例。在一個相關的說明中,一個值得關注的 Swift 提案 SE-216: 「Introduce User-defined Dynamically ‘callable’ Types動態成員查找 的近親,其中介紹了 dynamicCallable 註解。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索