细说Swift Dynamic Member Lookup

3,074 阅读4分钟

Swift 5.1 中引入了 @dynamicMemberLookup 属性包装器,它可以让我们在使用点语法访问类或结构体的某些值或者通过下标访问字典或其他容器的值时不需要使用具体的属性或下标语法。接下来将从以下几个方面对 @dynamicMemberLookup 进行详细分析。

语法特性

@dynamicMemberLookup 是一个属性包装器 (Property Wrapper),它可以用于类、结构体和枚举上,用于实现动态成员的访问。它的语法如下所示:

@dynamicMemberLookup
struct SomeStruct {
    subscript(dynamicMember member: String) -> Int {
        // ...
    }
}

其中,@dynamicMemberLookup 属性包装器需要放在结构体、类或枚举的定义前面。在上面的示例中,我们为 SomeStruct 结构体添加了一个 subscript(dynamicMember:) -> Int 的下标方法,该下标方法接受一个字符串类型的成员名称,并返回一个整数。

我们可以通过点语法来访问 SomeStruct 结构体的成员,就像访问常规属性一样,例如:

var structInstance = SomeStruct()
let someValue = structInstance.someMember // 点语法访问成员

作用

在许多情况下,我们需要在运行时访问「不存在」的属性或者下标,而这些属性或下标可能是基于某些条件动态生成的。在这种情况下,使用 @dynamicMemberLookup 可以实现动态访问成员,避免了在编写代码时就必须知道属性或下标名称的烦恼。

使用 @dynamicMemberLookup,我们可以在更为动态的环境中编写代码,同时也可以提供一种更为友好的语法,因此通常可以:

  1. 减少代码的复杂度。
  2. 增强代码的可读性和维护性。
  3. 减少因为类型转换和判断而导致的运行时错误。

使用场景

下面是 @dynamicMemberLookup 可以用到的几个场景:

  1. 访问 JSON 数据

在解析 JSON 数据时,我们通常需要访问 JSON 数据的成员,这时使用 @dynamicMemberLookup 属性包装器可以方便地实现动态访问 JSON 数据。例如:

@dynamicMemberLookup
struct JSON {
    private let value: Any
    
    init(_ value: Any) {
        self.value = value
    }
    
    subscript(dynamicMember member: String) -> JSON {
        if let dict = value as? [String: Any], let value = dict[member] {
            return JSON(value)
        } else {
            return JSON(NSNull())
        }
    }
    
    subscript(index: Int) -> JSON {
        if let array = value as? [Any], array.indices.contains(index) {
            return JSON(array[index])
        } else {
            return JSON(NSNull())
        }
    }
}

let json = JSON(["name": "Tom", "age": 20, "hobbies": ["reading", "swimming"]])
print(json.name) // 输出 "Tom"
print(json.age) // 输出 20
print(json.hobbies[1]) // 输出 "swimming"

在上面的例子中,我们使用 @dynamicMemberLookup 属性包装器为 JSON 结构体动态添加了属性,用于访问 JSON 数据的成员。

  1. 动态 Swift KeyPaths

使用 Swift 的 KeyPath 可以让我们方便地访问对象的属性或方法,而使用 @dynamicMemberLookup 属性包装器可以实现通过字符串访问 KeyPath。例如:

@dynamicMemberLookup
struct Person {
    var name: String
    var age: Int
    
    subscript(dynamicMember member: String) -> KeyPath<Person, String>? {
        if member == "name" {
            return .name
        } else {
            return nil
        }
    }
}

let person = Person(name: "Tom", age: 20)
let nameKeyPath = person.name
let name = person[keyPath: nameKeyPath] // 输出 "Tom"

在上面的例子中,我们使用 @dynamicMemberLookup 属性包装器为 Person 结构体动态添加了属性,用于访问 KeyPath

  1. 访问未知类型的对象

在一些动态类型的语言中,经常会出现需要访问一个未知类型的对象的属性或方法的场景。在 Swift 中,我们可以使用 @dynamicMemberLookup 属性包装器来实现类似的访问。例如:

@dynamicMemberLookup
struct AnyValue {
    private let value: Any
    
    init(_ value: Any) {
        self.value = value
    }
    
    subscript<T>(dynamicMember member: String) -> T? {
        return (value as? [String: Any])?[member] as? T
    }
}

let dict = ["name": "Tom", "age": 20, "hobbies": ["reading", "swimming"]]
let anyValue = AnyValue(dict)
let name = anyValue.name // 输出 "Tom"
let age = anyValue.age // 输出 20
let hobbies = anyValue.hobbies // 输出 ["reading", "swimming"]

在上面的例子中,我们使用 @dynamicMemberLookup 属性包装器为 AnyValue 结构体动态添加了属性,用于访问某个属性或方法并返回一个未知类型的值。

使用陷阱

在使用 @dynamicMemberLookup 时,需要注意以下几个陷阱:

  1. dynamicMember 必须是字符串类型,不能使用其他类型,如整数或枚举定义等。
  2. 使用 @dynamicMemberLookup 最好是最后的手段,因为在编写代码时必须动态推测访问的对象的结构,从而可能导致意外行为。
  3. 尽管 @dynamicMemberLookup 可以让代码看起来更简洁,但是有时它可能会隐藏一些性能问题,如频繁的类型转换和判断会导致性能低下。