Swift 中的动态成员查找

文章目录

    • 前言
    • 基础介绍
    • 基础示例
      • [1. 定义一个动态成员访问类:](#1. 定义一个动态成员访问类:)
      • [2. 访问嵌套动态成员:](#2. 访问嵌套动态成员:)
    • [使用 KeyPath 的编译时安全性](#使用 KeyPath 的编译时安全性)
    • [KeyPath 用法示例](#KeyPath 用法示例)
    • [KeyPath 进阶使用示例](#KeyPath 进阶使用示例)
      • [1. 动态访问属性:](#1. 动态访问属性:)
      • [2. 结合可选属性和 KeyPath:](#2. 结合可选属性和 KeyPath:)
      • [3. 动态 KeyPath 和字典:](#3. 动态 KeyPath 和字典:)
    • 总结

前言

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

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

基础介绍

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

Swift 复制代码
struct Cache {
    var storage: [String: Data] = [:]
}

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

Swift 复制代码
var cache = Cache()
let profile = cache.storage["profile"]

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

Swift 复制代码
@dynamicMemberLookup
struct Cache {
    private var storage: [String: Data] = [:]

    subscript(dynamicMember key: String) -> Data? {
        storage[key]
    }
}

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

Swift 复制代码
var cache = Cache()
let profile = cache.profile

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

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

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

基础示例

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

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

swift 复制代码
@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()

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

2. 访问嵌套动态成员:

swift 复制代码
@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

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

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

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

使用 KeyPath 的编译时安全性

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

Swift 复制代码
@dynamicMemberLookup
final class Store\<State, Action>: 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<T>(dynamicMember keyPath: KeyPath<State, T>) -> T {
        state[keyPath: keyPath]
    }

    func send(_ action: Action) {
        state = reduce(state, action)
    }

}

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

Swift 复制代码
struct State {
var products: \[String] = \[]
var isLoading = false
}

enum Action {
case fetch
}

let store: Store\<State, Action> = .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

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

KeyPath 用法示例

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

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

swift 复制代码
struct Person {
    let name: String
    let age: Int
}

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

swift 复制代码
let person = Person(name: "Alice", age: 30)

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

swift 复制代码
let nameKeyPath = \Person.name
let ageKeyPath = \Person.age

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

swift 复制代码
let personName = person[keyPath: nameKeyPath] // "Alice"
let personAge = person[keyPath: ageKeyPath]   // 30

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

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

swift 复制代码
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]

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

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

swift 复制代码
let sortedPeople = people.sorted(by: \.age)

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

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

KeyPath 进阶使用示例

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

1. 动态访问属性:

swift 复制代码
let propertyName = "name"
let dynamicKeyPath = \Person[keyPath: propertyName] // Error: Cannot convert value of type 'String' to expected key path type 'WritableKeyPath<Person, _>'

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

2. 结合可选属性和 KeyPath:

swift 复制代码
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")
}

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

3. 动态 KeyPath 和字典:

swift 复制代码
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")
}

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

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

总结

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

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

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

相关推荐
程序猿看视界41 分钟前
如何在 UniApp 中实现 iOS 版本更新检测
ios·uniapp·版本更新
dr李四维4 小时前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
️ 邪神4 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】自定义View
flutter·ios·鸿蒙·reactnative·anroid
比格丽巴格丽抱16 小时前
flutter项目苹果编译运行打包上线
flutter·ios
网络安全-老纪17 小时前
iOS应用网络安全之HTTPS
web安全·ios·https
今天啥也没干19 小时前
使用 Sparkle 实现 macOS 应用自定义更新弹窗
前端·javascript·swift
1024小神20 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
lzhdim21 小时前
iPhone 17 Air看点汇总:薄至6mm 刷新苹果轻薄纪录
ios·iphone
安和昂21 小时前
【iOS】知乎日报第四周总结
ios
yngsqq1 天前
037集——JoinEntities连接多段线polyline和圆弧arc(CAD—C#二次开发入门)
开发语言·c#·swift