细说 Swift 4.2 新特性:Dynamic Member Lookup

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
top Created with Sketch.