Swift 4.2 的新特性這兩篇文章已經介紹的很清楚了:WWDC 2018:Swift 更新了什麼,Swift 4.2 新特性更新。但是 4.2 中實現的 dynamic member lookup 蘋果在 WWDC 上卻完全沒有提到。然而我認為這是一個對未來有著重要影響的特性,所以這裡單獨介紹一下。
語法
這個特性中文可以叫動態查詢成員。在使用@dynamicMemberLookup
標記了物件後(物件、結構體、列舉、protocol),實現了subscript(dynamicMember member: String)
方法後我們就可以訪問到物件不存在的屬性。如果訪問到的屬性不存在,就會呼叫到實現的 subscript(dynamicMember member: String)
方法,key 作為 member 傳入這個方法。
比如我們宣告瞭一個結構體,沒有宣告屬性。
@dynamicMemberLookup
struct Person {
subscript(dynamicMember member: String) -> String {
let properties = ["nickname": "Zhuo", "city": "Hangzhou"]
return properties[member, default: "undefined"]
}
}
//執行以下程式碼
let p = Person()
print(p.city)
print(p.nickname)
print(p.name)
複製程式碼
如果沒有宣告@dynamicMemberLookup
的話,執行的程式碼肯定會編譯失敗。很顯然作為一門型別安全語言,編譯器會告訴你不存在這些屬性。但是在宣告瞭@dynamicMemberLookup
後,雖然沒有定義 city
等屬性,但是程式會在執行時動態的查詢屬性的值,呼叫subscript(dynamicMember member: String)
方法來獲取值。
這樣安全嗎?
Swift 面世時就大談自己的安全特性,現在來了這麼一個無限制訪問的成員萬一返回的是nil
不就閃退了?是的,出於安全的原因,如果實現了這個特性,你就不能返回可選值。必須處理好意料外的情況,一定要有值返回。不像常規的subscript
方法可以返回可空的值。
說好的動態查詢,如果兩個屬性型別不一樣怎麼破
這個方法可以被過載。和泛型的邏輯類似,會根據你要的返回值而通過型別推斷來選擇對應的subscript
方法。
@dynamicMemberLookup
struct Person {
subscript(dynamicMember member: String) -> String {
let properties = ["nickname": "Zhuo", "city": "Hangzhou"]
return properties[member, default: "undefined"]
}
subscript(dynamicMember member: String) -> Int {
return 18
}
}
複製程式碼
但是執行的時候就一定要告訴編譯器你要獲取的屬性是什麼型別的,否則會編譯錯誤。
let p = Person()
let age: Int = p.age
print(age) // 18
複製程式碼
Swift 中函式是一等公民,所以返回函式也是可以的。
@dynamicMemberLookup
struct Person {
subscript(dynamicMember member: String) -> (_ input: String) -> Void {
return {
print("Hello! I live at the address \($0).")
}
}
}
複製程式碼
居然可以繼承!
需要注意的是如果宣告在類上,那麼他的子類也會具有動態查詢成員的能力。
@dynamicMemberLookup
class User {
subscript(dynamicMember member: String) -> String {
return "user"
}
}
class Developer: User { }
let dev = Developer()
dev.name // "user"
複製程式碼
雖然想起來應該是這樣,但是還是很反直覺。因為大多數開發者沒想過繼承一個類後,會有失去屬性拼寫檢查的副作用。這樣可能不小心寫錯了屬性的名字編譯器也不會告訴你。
所以宣告在類上的時候一定要特別謹慎。
當然如果想害同事,在BaseViewController
裡宣告是個好主意。
看起來很騷有什麼卵用?
這個特性的感覺就是乍一看很厲害的樣子,仔細一看好像就這麼回事,再冷靜想想似乎沒有這麼簡單。
這個東西本質上只是一個語法糖,和陣列的subscript
類似。
let numbers = [1, 2]
let firstItem = number[0]
//這個語法最後還是呼叫到了一個方法,如果沒有這種寫法,類似 oc 的時候就需要顯式的呼叫一個方法
NSNumber *firstItem = [numnber obbjectAtIndex: 0];
複製程式碼
原來你需要顯式宣告字串引數的地方,可以不用是字串的形式,可以直接用點語法訪問。官方舉的例子是 JSON 的使用。 常規的寫法是這樣的:
json[0]?["name"]?["first"]?.stringValue
複製程式碼
如果像這樣定義動態查詢成員:
@dynamicMemberLookup
enum JSON {
case intValue(Int)
case stringValue(String)
case arrayValue(Array<JSON>)
case dictionaryValue(Dictionary<String, JSON>)
var stringValue: String? {
if case .stringValue(let str) = self {
return str
}
return nil
}
subscript(index: Int) -> JSON? {
if case .arrayValue(let arr) = self {
return index < arr.count ? arr[index] : nil
}
return nil
}
subscript(key: String) -> JSON? {
if case .dictionaryValue(let dict) = self {
return dict[key]
}
return nil
}
subscript(dynamicMember member: String) -> JSON? {
if case .dictionaryValue(let dict) = self {
return dict[member]
}
return nil
}
}
複製程式碼
那麼寫起來就會是這樣:
json[0]?.name?.first?.stringValue
複製程式碼
實現方案
這個功能的實現原理很簡單,就是編譯器幫助你把點語法轉化為下標的語法:
a = someValue.someMember
//編譯器處理後
a = someValue[dynamicMember: "someMember"]
複製程式碼
動態屬性其實並不陌生,回憶一下 OC 裡的屬性就是動態合成的。宣告瞭@property
後,編譯器幫你生成get、set方法。與之類似,在宣告瞭動態查詢成員後,編譯器幫你轉換成了對應的方法。
然而事情並沒有這麼簡單
如果你以為這只是一個語法糖,那你就錯了。
獨有的身世暴露了你
這個 pr 是由已經離開蘋果加入谷歌的 swift 創始人 CL 提出的。他不僅提了這個 pr,而且還自己實現了。果然是 swift 是親兒子,身在曹營還不忘為 swift 添磚加瓦。而且大佬不僅提了這個,還提了一個 @dynamicCallable 。
當你給一個物件標記@dynamicCallable
後,可以動態的給傳參。
// 常規操作
a = someValue(keyword1: 42, "foo", keyword2: 19)
// dynamicallyCall
a = someValue.dynamicallyCall(withKeywordArguments: [
"keyword1": 42, "": "foo", "keyword2": 19
])
複製程式碼
是的,這很 JS。
大佬就是大佬,要啥有啥。目前 @dynamicCallable
的進度已經在 review 中,也許 5.0 的時候能夠上?我猜測 swift 團隊想這兩個特性都開發完後一起宣佈所以這次釋出會沒有介紹。
另有所謀:把 Python 和 JS 納入懷中
Swift 目前可以”良好“的和 C、OC 互動。然而程式的世界裡還有一些重要的動態語言,比如 Python 、 JS,emmm,還有有實力但是不太主流的 Perl、Ruby。如果 swift 能夠愉快的的呼叫 Python 和 JS 的庫,那麼毫無疑問會極大的擴充的 swift 的邊界。
這裡需要一點想象力,因為這個設計真正的意義是@dynamicMemberLookup
、 @dynamicCallable
組合起來用。通過@dynamicMemberLookup
動態的返回一個函式,再通過@dynamicCallable
來呼叫。從語法層面來講,這種姿態下 swift 完完全全是一門動態語言。
@dynamicCallable @dynamicMemberLookup
class WeiSuoYuWei {
}
let niuBi = WeiSuoYuWei()
niuBi.someMethod.dynamicallyCall(withKeywordArguments: ["wei_suo_yu_wei": true])
複製程式碼
就像上面的程式碼展示的,你不必宣告過someMethod
也可以通過動態特性呼叫到,合法的傳參。真的可以為所欲為!
據說谷歌的 TensorFlow For Swift 能夠順利的開發就是依靠了這個特性。CL 是這麼說的:
While this is a syntactic sugar proposal, we believe that this expands Swift to be usable in important new domains
語法糖上的一小步,swift 的一大步!
reference
How to use Dynamic Member Lookup in Swift – Hacking with Swift
SE 195: Introduce User-defined “Dynamic Member Lookup” Types