如何在泛型结构体中存储属性并实现UserDefaults持久化
问题场景
开发可复用远程数据拉取组件时,遇到泛型多类型兼容问题,具体信息如下:
- 主应用内定义了遵循
Codable的配置结构体RemoteData,承载多组实验配置:
// in my app struct RemoteData: Codable { var experimentOne: [Variant<[Page]>] var experimentTwo: [Variant<Bool>] var experimentThree: [Variant<String>] }
- 公共组件包内定义泛型实验项结构
Variant<T>,泛型参数T遵循Codable:
// in my package public struct Variant<T: Codable>: Codable, VariantProtocol { public var experimentName: String public var variantName: String public var percent: Int public var value: T }
- 组件包内核心拉取类
DataFetcher通过泛型兼容不同返回结构,业务侧通过子类化传入具体类型、配置拉取地址:
// in my package, subclassed in app to provide target url open class DataFetcher<T: Decodable> { var remoteConfig: T? var url: URL public init(url: String) { self.url = URL(string: url)! } func fetchAndSaveData() { // 待实现:拉取数据、过滤用户命中的实验项、存储到UserDefaults } }
预期存储逻辑:拉取数据完成后先按流量规则过滤,得到当前用户命中的实验项,优先以「实验名称为key、命中项对应value为value」的结构存入UserDefaults,退而求其次可存储完整过滤后的数组。
当前卡点:DataFetcher仅通过泛型做类型约束,无法提前感知传入模型的具体属性结构,无法内置通用存储逻辑。最初尝试定义协议约束泛型参数、让RemoteData遵循协议的方案无法生效——如果协议要求返回Variant数组,必须指定明确的泛型T类型,无法兼容RemoteData内同时存在的多种不同泛型参数的Variant数组属性。
可行实现方案
核心思路是把和具体类型绑定的定制存储逻辑,从通用组件层下沉到业务侧的具体模型实现中,组件层只做流程调度,通过协议+类型抹除避开泛型参数不统一的问题。
第一步:组件层定义无泛型约束的存储协议
在公共组件包内定义聚合协议,不涉及任何具体泛型类型,只要求遵循者返回最终可直接存入UserDefaults的内容:
// in package import Foundation public protocol RemoteConfigStorable { /// 返回所有待存储的键值对:key为实验名称,value为用户命中的实验项对应值 func storageKeyValues() -> [String: Any] /// 返回过滤后所有用户命中的实验项(备选完整存储方案使用) func matchedVariants() -> [Any] }
同时给VariantProtocol扩展默认的流量命中判断逻辑,所有Variant实现可以直接复用:
// in package public extension VariantProtocol { func isUserMatched(customPercent: Int? = nil) -> Bool { // 替换为实际的分桶逻辑,默认取0-99随机值匹配流量占比 let userPercent = customPercent ?? Int.random(in: 0..<100) return userPercent < percent } }
第二步:修改DataFetcher的泛型约束,增加通用存储逻辑
给DataFetcher的泛型参数增加RemoteConfigStorable约束,拉取到数据后直接调用协议方法拿到可存储内容,不需要感知T的具体结构:
// in package open class DataFetcher<T: Decodable & RemoteConfigStorable> { var remoteConfig: T? var url: URL private let defaults = UserDefaults.standard public init(url: String) { guard let targetUrl = URL(string: url) else { fatalError("Invalid fetch url") } self.url = targetUrl } public func fetchAndSaveData() { // 省略原有网络请求、JSON解码为remoteConfig的逻辑 guard let loadedConfig = remoteConfig else { return } // 优先存储键值对结构 let storageItems = loadedConfig.storageKeyValues() storageItems.forEach { key, value in defaults.set(value, forKey: key) } // 如果需要存储完整命中数组,可在这里对loadedConfig.matchedVariants()做归档存储 } }
第三步:业务侧模型实现协议即可接入
主应用内的RemoteData只需要遵循RemoteConfigStorable,实现对应方法完成自身属性的过滤逻辑,不需要修改组件包任何代码:
// in app extension RemoteData: RemoteConfigStorable { public func storageKeyValues() -> [String : Any] { var result = [String: Any]() // 逐个实验数组过滤命中项 if let matchedExp1 = experimentOne.first(where: { $0.isUserMatched() }) { result[matchedExp1.experimentName] = matchedExp1.value } if let matchedExp2 = experimentTwo.first(where: { $0.isUserMatched() }) { result[matchedExp2.experimentName] = matchedExp2.value } if let matchedExp3 = experimentThree.first(where: { $0.isUserMatched() }) { result[matchedExp3.experimentName] = matchedExp3.value } return result } public func matchedVariants() -> [Any] { var result = [Any]() result.append(contentsOf: experimentOne.filter { $0.isUserMatched() }) result.append(contentsOf: experimentTwo.filter { $0.isUserMatched() }) result.append(contentsOf: experimentThree.filter { $0.isUserMatched() }) return result } }
方案优势
- 组件层完全和具体业务模型结构解耦,不需要感知模型有多少个属性、每个属性对应的Variant泛型类型是什么,符合开闭原则
- 不存在泛型类型冲突问题,不管Variant包裹的是基础类型还是自定义复杂类型,都可以正常通过类型抹除存入集合
- 后续新增实验字段时,只需要在
RemoteData的协议实现中增加对应分支即可,不需要修改通用组件逻辑 - 其他业务场景的远程配置模型只要实现
RemoteConfigStorable协议,即可直接复用DataFetcher的拉取、存储能力,不需要重复开发
内容的提问来源于stack exchange,提问作者mergesort
相关产品推荐
相关产品推荐

