• Swift 中的动态成员查找


    请添加图片描述

    在这里插入图片描述

    前言

    在 Swift 中,动态成员查找是一种允许在编译时未知成员的情况下,通过字符串名称来访问属性和方法的机制。这在需要与动态数据进行交互,或者在某些情况下进行元编程时非常有用。动态成员查找通过 Swift 的 @dynamicMemberLookup 特性实现。

    Glassfy:简化构建、管理和推广应用内购买。从订阅管理 SDK 到付费墙等完整的货币化工具。立即免费构建。

    基础介绍

    假设我们正在开发一个提供缓存功能的类型,并将其建模为名为 Cache 的结构体。

    struct Cache {
        var storage: [String: Data] = [:]
    }
    
    • 1
    • 2
    • 3

    为了访问缓存的数据,我们调用存储属性的下标,该存储属性是 Dictionary 类型提供的。

    var cache = Cache()
    let profile = cache.storage["profile"]
    
    • 1
    • 2

    在这里没有什么特别之处。我们像以前一样通过 Dictionary 类型的下标访问字典。让我们看看如何使用 @dynamicMemberLookup 属性改进 Cache 类型的 API。

    @dynamicMemberLookup
    struct Cache {
        private var storage: [String: Data] = [:]
    
        subscript(dynamicMember key: String) -> Data? {
            storage[key]
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    如上例所示,我们使用 @dynamicMemberLookup 属性标记了 Cache 类型。我们必须实现具有 dynamicMember 参数并返回我们需要的任何内容的下标。

    var cache = Cache()
    let profile = cache.profile
    
    • 1
    • 2

    现在,我们可以更方便地访问 Cache 类型的配置文件数据。我们的 API 的使用者可能会认为配置文件是 Cache 类型的属性,但事实并非如此。

    此特性完全在运行时工作,并利用了在点符号后键入的任何属性名称来访问 Cache 类型的下标,该下标具有 dynamicMember 参数。

    整个逻辑在运行时运行,编译期间的结果是不确定的。在运行时,完全可以决定应该从下标返回哪些数据以及如何处理 dynamicMember 参数。

    基础示例

    以下是在 Swift 中使用动态成员查找的一些基本示例:

    1. 定义一个动态成员访问类:

    @dynamicMemberLookup
    struct DynamicMemberExample {
        subscript(dynamicMember member: String) -> String {
            return "Accessed dynamic member: \(member)"
        }
    }
    
    // 创建实例
    let dynamicObject = DynamicMemberExample()
    
    // 使用动态成员查找
    print(dynamicObject.someProperty) // 输出: Accessed dynamic member: someProperty
    print(dynamicObject.someMethod()) // 输出: Accessed dynamic member: someMethod()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在上面的示例中,我们定义了一个结构体 DynamicMemberExample,使用 @dynamicMemberLookup 注解。这允许我们通过下标方法 subscript(dynamicMember:) 来实现对动态成员的访问。然后我们可以像使用普通成员一样使用动态成员。

    2. 访问嵌套动态成员:

    @dynamicMemberLookup
    struct NestedDynamicMemberExample {
        struct Inner {
            subscript(dynamicMember member: String) -> String {
                return "Accessed dynamic member: \(member)"
            }
        }
        
        var inner = Inner()
    }
    
    // 创建实例
    let nestedDynamicObject = NestedDynamicMemberExample()
    
    // 使用动态成员查找
    print(nestedDynamicObject.inner.someProperty) // 输出: Accessed dynamic member: someProperty
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这个示例中,我们在一个嵌套的结构体 Inner 中实现了动态成员查找。然后我们可以通过外部结构体 NestedDynamicMemberExample 的实例访问嵌套结构体的动态成员。

    动态成员查找是一种强大的特性,但也要注意,它通常会损失一些类型安全性,因为在编译时不会检查动态成员的存在。使用动态成员查找时,请确保仔细考虑潜在的风险。

    请注意,动态成员查找在 Swift 5.1 及更高版本中可用。如果在更早的版本中使用动态成员查找,可能需要进行适当的版本升级。

    使用 KeyPath 的编译时安全性

    我们唯一能找到的缺点是缺乏编译时安全性。我们可以将 Cache 类型视为代码中键入的任何属性名称。幸运的是,@dynamicMemberLookup 下标的参数不仅可以是 String 类型,还可以是 KeyPath 类型。

    @dynamicMemberLookup
    final class Store\: ObservableObject {
    typealias ReduceFunction = (State, Action) -> State
    
        @Published private var state: State
        private let reduce: ReduceFunction
    
        init(
            initialState state: State,
            reduce: @escaping ReduceFunction
        ) {
            self.state = state
            self.reduce = reduce
        }
    
        subscript(dynamicMember keyPath: KeyPath) -> T {
            state[keyPath: keyPath]
        }
    
        func send(_ action: Action) {
            state = reduce(state, action)
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    如上例所示,我们定义了接受强类型 KeyPath 实例的 dynamicMember 参数下标。在这种情况下,我们允许 State 类型的 KeyPath,这有助于我们获得编译时安全性。因为每当我们传递与 State 类型无关的错误 KeyPath 时,编译器都会显示错误。

    struct State {
    var products: \[String] = \[]
    var isLoading = false
    }
    
    enum Action {
    case fetch
    }
    
    let store: Store\ = .init(initialState: .init()) { state, action in
    var state = state
    switch action {
    case .fetch:
    state.isLoading = true
    }
    return state
    }
    
    print(store.isLoading)
    print(store.products)
    print(store.favorites) // Compiler error
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在上例中,我们通过接受 KeyPath 的下标访问 Store 的私有 state 属性。这看起来与前面的例子类似,但在这种情况下,只要尝试访问 State 类型的不可用属性,编译器就会显示错误。

    KeyPath 用法示例

    当涉及到 Swift 中的 KeyPath 时,实际的代码示例可以更好地阐明其概念和用法。下面将详细介绍先前提到的示例代码,以更清晰地展示 KeyPath 的用途和编译时安全性。

    首先,我们定义一个名为 Person 的结构体,表示一个人的姓名和年龄:

    struct Person {
        let name: String
        let age: Int
    }
    
    • 1
    • 2
    • 3
    • 4

    接下来,我们创建一个 Person 实例,名为 person,并为其赋予姓名 “Alice” 和年龄 30:

    let person = Person(name: "Alice", age: 30)
    
    • 1

    然后,我们使用 KeyPath 来引用 Person 实例的属性。我们创建了两个 KeyPath,一个用于访问姓名属性,另一个用于访问年龄属性:

    let nameKeyPath = \Person.name
    let ageKeyPath = \Person.age
    
    • 1
    • 2

    使用 KeyPath,我们可以通过下标语法从 person 实例中访问属性值:

    let personName = person[keyPath: nameKeyPath] // "Alice"
    let personAge = person[keyPath: ageKeyPath]   // 30
    
    • 1
    • 2

    在这里,我们通过 nameKeyPathageKeyPath 使用了 KeyPath,这是编译时类型安全的。如果我们尝试引用不存在的属性,编译器将在编译时捕获错误。

    接下来,我们假设有一个数组 people 包含多个 Person 实例。我们可以使用 KeyPath 来对数组中的实例进行映射,以获取所有人的姓名和年龄:

    let people = [
        Person(name: "Alice", age: 30),
        Person(name: "Bob", age: 25),
        Person(name: "Charlie", age: 28)
    ]
    
    let names = people.map(\.name) // ["Alice", "Bob", "Charlie"]
    let ages = people.map(\.age)   // [30, 25, 28]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    这里,我们使用 KeyPath 进行了数组的映射,从每个 Person 实例中提取了姓名和年龄。

    最后,我们可以使用 KeyPath 对数组中的实例按年龄进行排序:

    let sortedPeople = people.sorted(by: \.age)
    
    • 1

    通过传递 KeyPathsorted(by:) 方法,我们可以按年龄对人员进行排序。

    总之,KeyPath 是 Swift 中的一项强大特性,它提供了类型安全的属性和下标访问方式,可以在编译时捕获错误,并具有映射、排序等便利功能。通过理解 KeyPath 的概念和用法,可以写出更安全、更易读和更高效的 Swift 代码。

    KeyPath 进阶使用示例

    下面介绍一下关于 KeyPath 更多的用法和示例

    1. 动态访问属性:

    let propertyName = "name"
    let dynamicKeyPath = \Person[keyPath: propertyName] // Error: Cannot convert value of type 'String' to expected key path type 'WritableKeyPath'
    
    • 1
    • 2

    在这个示例中,我们尝试使用字符串变量创建一个动态的 KeyPath。然而,这将会导致编译错误,因为编译器需要在编译时确定 KeyPath 的类型。

    2. 结合可选属性和 KeyPath:

    struct Team {
        let captain: Person?
    }
    
    let team = Team(captain: Person(name: "David", age: 32))
    
    if let captainName = team.captain?[keyPath: \.name] {
        print("Captain's name: \(captainName)")
    } else {
        print("No captain assigned")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    在这个示例中,我们定义了一个 Team 结构体,其中的 captain 属性是可选的 Person 实例。通过使用 KeyPath,我们可以在安全的情况下访问可选属性。

    3. 动态 KeyPath 和字典:

    let personKeyPath: KeyPath<Person, String> = \.name
    
    let personDictionary = ["name": "Ella", "age": "28"]
    if let name = personDictionary[keyPath: personKeyPath] {
        print("Name from dictionary: \(name)")
    } else {
        print("Name not found in dictionary")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这个示例中,我们将 KeyPath 与字典结合使用。尽管字典中的键和 KeyPath 类型可能不匹配,但在运行时可以使用动态 KeyPath 访问字典中的值。

    KeyPath 是 Swift 中非常有用的一项功能,它提供了类型安全的属性和下标访问方法,可以在编译时捕获错误,还支持映射、排序和与其他类型的组合使用。理解 KeyPath 的概念和用法将为编写更清晰、更安全的 Swift 代码提供强大的工具。

    总结

    KeyPath 是 Swift 编程语言中的一项强大功能,它为属性和下标提供了类型安全的访问方式,提升了代码的可读性、可维护性和性能。通过使用 KeyPath,我们可以避免使用字符串进行属性访问,从而减少了因拼写错误而引发的问题。

    通过深入理解 KeyPath 的概念和用法,开发者可以编写更具类型安全性、可读性和性能的 Swift 代码。这一功能不仅简化了代码编写,还为我们提供了一种更加优雅的方式来处理属性和下标的访问与操作。

    我们学习了如何使用 @dynamicMemberLookup 属性改进特定类型的 API。虽然并不是每个类型都需要它,但可以谨慎使用它来改善 API。

  • 相关阅读:
    C_C++在linux和windows下文件操作比较总结
    PG14归档失败解决办法archiver failed on wal_lsn
    redis为什么用跳表而不用平衡树
    EBI、DDD及其演变架构史
    Js写的二级联动和三级联动
    JAVA SpringMVC老项目集成knife4j
    刷爆力扣之矩阵中的幻方
    蓝桥杯备赛(三)
    代码随想录第34天: 贪心part03
    TCP流套接字编程
  • 原文地址:https://blog.csdn.net/qq_36478920/article/details/132577897