細說 Swift 4.2 新特性:Dynamic Member Lookup

Swift 4.2 的新特性這兩篇文章已經介紹的很清楚了:WWDC 2018:Swift 更新了什麼Swift 4.2 新特性更新。可是 4.2 中實現的 dynamic member lookup 蘋果在 WWDC 上卻徹底沒有提到。然而我認爲這是一個對將來有着重要影響的特性,因此這裏單獨介紹一下。python

語法

這個特性中文能夠叫動態查找成員。在使用@dynamicMemberLookup標記了對象後(對象、結構體、枚舉、protocol),實現了subscript(dynamicMember member: String)方法後咱們就能夠訪問到對象不存在的屬性。若是訪問到的屬性不存在,就會調用到實現的 subscript(dynamicMember member: String)方法,key 做爲 member 傳入這個方法。 好比咱們聲明瞭一個結構體,沒有聲明屬性。git

@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方法能夠返回可空的值。github

說好的動態查找,若是兩個屬性類型不同怎麼破

這個方法能夠被重載。和泛型的邏輯相似,會根據你要的返回值而經過類型推斷來選擇對應的subscript方法。json

@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
    }
}
複製代碼

可是執行的時候就必定要告訴編譯器你要獲取的屬性是什麼類型的,不然會編譯錯誤。swift

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"
複製代碼

雖然想起來應該是這樣,可是仍是很反直覺。由於大多數開發者沒想過繼承一個類後,會有失去屬性拼寫檢查的反作用。這樣可能不當心寫錯了屬性的名字編譯器也不會告訴你。

因此聲明在類上的時候必定要特別謹慎。app

固然若是想害同事,在BaseViewController裏聲明是個好主意。 dom

看起來很騷有什麼卵用?

這個特性的感受就是乍一看很厲害的樣子,仔細一看好像就這麼回事,再冷靜想一想彷佛沒有這麼簡單。

這個東西本質上只是一個語法糖,和數組的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


相關文章
相關標籤/搜索