首先,Swift的@dynamicMemberLookup属性似乎是一个奇怪的特性,因为它可以用来规避Swift非常强调的类型安全。
本质上,向类或结构添加该属性使我们能够添加对访问该类型上的任何属性的支持 -不管该属性是否实际存在。 例如,这里有一个设置类型,目前实现@dynamicMemberLookup像这样:
@dynamicMemberLookup
struct Settings {
var colorTheme = ColorTheme.modern
var itemPageSize = 25
var keepUserLoggedIn = true
subscript(dynamicMember member: String) -> Any? {
switch member {
case "colorTheme":
return colorTheme
case "itemPageSize":
return itemPageSize
case "keepUserLoggedIn":
return keepUserLoggedIn
default:
return nil
}
}
}
由于上述类型支持动态成员查找,我们可以在访问其属性时使用任意名称,并且当没有声明的属性匹配该名称时,编译器不会给出任何警告或错误:
let settings = Settings()
let theme = settings.colorTheme
let somethingUnknown = settings.somePropertyName
同样,这似乎是Swift支持的一个奇怪特性,但在编写Swift和更动态语言(如Ruby、Python或JavaScript)之间的连接代码时,或者在编写其他类型的基于代理的代码时,它是非常有用的。
然而,还有一种使用@dynamicMemberLookup的方法,即使在完全静态的Swift代码中也非常有用——那就是将它与键路径结合起来。
作为一个例子,让我们重新访问“Swift中的结合值和引用类型”中的引用类型(它允许将值类型作为引用传递),并添加对动态查找其包装值类型成员之一的支持,但这一次使用的是KeyPath,而不是字符串:
@dynamicMemberLookup
class Reference<Value> {
private(set) var value: Value
init(value: Value) {
self.value = value
}
subscript<T>(dynamicMember keyPath: KeyPath<Value, T>) -> T {
value[keyPath: keyPath]
}
}
现在,这真的很酷,因为上面的操作使我们能够直接访问任何我们的值类型的属性,就像它们是我们引用类型本身的属性一样——像这样:
let reference = Reference(value: Settings())
let theme = reference.colorTheme
因为我们使用键路径实现了引用类型的dynamicMember下标,所以在使用它时不能像使用字符串时那样查找任意属性名。
我们甚至还可以添加一个可变版本,通过创建一个接受WritableKeyPath的下标重载,然后实现它的getter和setter:
extension Reference {
subscript<T>(dynamicMember keyPath: WritableKeyPath<Value, T>) -> T {
get { value[keyPath: keyPath] }
set { value[keyPath: keyPath] = newValue }
}
}
有了上面的内容,我们现在可以直接改变任何用我们的引用类型包装的值——就像我们改变引用实例本身一样:
let reference = Reference(value: Settings())
reference.theme = .oldSchool
最后,就像我们在原始文章中将所有突变api从Reference提取到一个新的mutablerereference类型一样-让我们在这里也这样做,以便能够限制代码库中可能发生突变的部分:
@dynamicMemberLookup
class Reference<Value> {
fileprivate(set) var value: Value
init(value: Value) {
self.value = value
}
subscript<T>(dynamicMember keyPath: KeyPath<Value, T>) -> T {
value[keyPath: keyPath]
}
}
class MutableReference<Value>: Reference<Value> {
subscript<T>(dynamicMember keyPath: WritableKeyPath<Value, T>) -> T {
get { value[keyPath: keyPath] }
set { value[keyPath: keyPath] = newValue }
}
}
使用上面的方法,我们现在可以轻松地将值类型作为引用传递,并像直接访问包装好的值一样读取和修改其属性——例如:
class ProfileViewModel {
private let user: User
private let settings: MutableReference<Settings>
init(user: User, settings: MutableReference<Settings>) {
self.user = user
self.settings = settings
}
func makeEmailAddressIcon() -> Icon {
// Reading Setting's 'colorTheme' property:
var icon = Icon.email
icon.useLightVersion = settings.colorTheme.isDark
return icon
}
func rememberMeSwitchToggled(to newValue: Bool) {
// Mutating Setting's 'keepUserLoggedIn' property:
settings.keepUserLoggedIn = newValue
}
}