You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在泛型结构体中存储属性并实现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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 20:01:07