細說 Swift 4.2 新特性:Dynamic Member Lookup

沒故事的卓同學發表於2018-06-16

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 4.2 新特性:Dynamic Member Lookup

這樣安全嗎?

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

細說 Swift 4.2 新特性:Dynamic Member Lookup
雖然想起來應該是這樣,但是還是很反直覺。因為大多數開發者沒想過繼承一個類後,會有失去屬性拼寫檢查的副作用。這樣可能不小心寫錯了屬性的名字編譯器也不會告訴你。

所以宣告在類上的時候一定要特別謹慎。

當然如果想害同事,在BaseViewController裡宣告是個好主意。

細說 Swift 4.2 新特性:Dynamic Member Lookup

看起來很騷有什麼卵用?

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

細說 Swift 4.2 新特性:Dynamic Member Lookup

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

細說 Swift 4.2 新特性:Dynamic Member Lookup

大佬就是大佬,要啥有啥。目前 @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


相關文章