[譯] Swift 中的動態特性

iWeslie發表於2018-11-27

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

作為一名忙碌的 Swift 開發人員,你的需求對你來說是特定的,但對所有人來說都是共同的。你希望編寫整潔的程式碼,一目瞭然地瞭解程式碼中的內容並快速解決無法預料的問題。

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

具體來說,你將學習以下內容:

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

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

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

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

入門

在開始之前,請先下載資源(入門專案和最終專案)。

為了讓你專注於學習 Swift 動態特性,其他所需的所有程式碼都已經為你寫好了!就像和一隻友好的導盲犬一起散步一樣,本教程將指導你完成入門程式碼中的所有內容。

[譯] Swift 中的動態特性

快樂的狗狗

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

使用 Mirror 的反射機制與除錯輸出

無論你是斷點除錯追蹤問題還是隻探索正在執行的程式碼,控制檯中的資訊是否整潔都會產生比較大的影響。Swift 提供了許多自定義控制檯輸出和捕獲關鍵事件的方法。對於自定義輸出,它沒有 Mirror 深入。Swift 提供比最強大的雪橇犬還要強大的力量,能把你從冰冷的雪地拉出來!

[譯] 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

[譯] Swift 中的動態特性

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

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),這也是一個強大的功能,可以使程式碼保持良好狀態。如前所述,有一個問題...

[譯] Swift 中的動態特性

“想抓我?”我全聽到了。

給狗編譯並完成程式碼

為了換取動態執行時的特性,你無法獲得依賴於 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 中的動態特性

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

後記

你可以使用本教程頂部的 下載材料 連結下載到專案的完整版本。

在本教程中,你利用了 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前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章