Swift5探究:@dynamicMemberLookup与@dynamicCallable

@dynamicMemberLookup

dynamicMemberLookup是Swift4.2里更新的一个特性翻译出来就是动态成员查找。在使用@dynamicMemberLookup标记了对象后(对象、结构体、枚举、protocol),实现了subscript(dynamicMember member: String)方法后我们就可以访问到对象不存在的属性。如果访问到的属性不存在,就会调用到实现的 subscript(dynamicMember member: String)方法,key 作为 member 传入这个方法。

//动态成员查找
@dynamicMemberLookup
struct Test {
    
    subscript (dynamicMember member: String) -> String {
        return "测试"
    }
    
    subscript (dynamicMember member: String) -> Int {
        return 321
    }
    
}
 let t = Test()
 let name:String = t.name
 let age:Int = t.age
 print(name,age)

输出结果为测试 321。可以看到,我在Test这个结构体里并没有声明name和age这两个属性,但是却依然获取了这两个属性,就是因为这个Test这个struct@dynamicMemberLookup标记之后,我们又实现了subscript (dynamicMember member: String) -> String这个方法之后,这就告诉程序要在运行时动态的查找属性的值,调用subscript (dynamicMember member: String) -> String方法获取值。注意:执行的时候要声明常量类型,否则编译会失败。
我们再来看下官方给出的demo:

@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里面的值则需要

let json = JSON.stringValue("Example")
json[0]?["name"]?["first"]?.stringValue

如果我们使用了@dynamicLookUp,就可以这么用json[0]?.name?.first?.stringValue

@dynamicCallable

Swift5又增加了@dynamicCallable,它是语法糖,而不是编译器魔法。它将类型标记为可直接调用,主要是想Swift代码更好的和Python、js等动态语言一起更好的工作。
要想使用@dynamicCallable,需要标记@dynamicCallable并实现一下方法:

func dynamicCall(withArguments args: [Int]) -> Double
func dynamicCall(withKeywordArguments args: KeyValuePairs <String,String>) -> String 

当你调用没有参数标签的类型(例如a(b,c))时会使用第一个,而当你提供标签时使用第二个(例如a(b:cat,c:dog))。@dynamicCallable对于接受和返回的数据类型非常灵活,使您可以从Swift的所有类型安全性中受益,同时还有一些高级用途。因此,对于第一种方法(无参数标签),您可以使用符合ExpressibleByArrayLiteral的任何内容,例如数组,数组切片和集合,对于第二种方法(使用参数标签),您可以使用符合ExpressibleByDictionaryLiteral的任何内容,例如字典和键值对。除了接受各种输入外,您还可以为输出提供多个重载,一个可能返回字符串,一个是整数,依此类推。只要Swift能够解决使用哪一个,你就可以混合搭配你想要的一切。代码:

@dynamicCallable
struct RandomNumber {
    func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Double {
        print("args:\(args)")
        let numberOfZeroes = Double(args.first?.value ?? 0)
        let maximum = pow(10, numberOfZeroes)
        return Double.random(in: 0...maximum)
    }
}
        let random = RandomNumber()
        let result = random(numberOfZeroes:5)
       print("result:\(result)")

使用@dynamicCallable时需要注意:可以将它应用于结构、枚举、类和协议,如果你实现withKeywordArguments:并且没有实现withArguments:,你的类型仍然可以在没有参数标签的情况下调用,你只需要将键设置为空字符串就行。如果withKeywordArguments:或withArguments:被标记为throw,则调用该类型也会throw。不能将@dynamicCallable添加到扩展,只能添加在类型的主要定义中。可以为类型添加其他方法和属性,并正常使用它们。另外,它不支持方法解析,这也就是说我们必须直接调用类,random(numberOfZeroes: 5),而不是调用类型上的方法(例如random.generate(numberOfZeroes: 5)。

推荐阅读更多精彩内容