Swift:用于存储键值的容器


假设您需要将用户ID保存在UserDefaults中 。 第一步是什么?


通常,企业从为密钥添加常量并检查其唯一性开始。 对于大多数其他键值存储区来说,这是正确的。 此类存储库的原始设计的结果不仅限于密钥;以非系统方法集的形式出现的接口会导致许多可能的问题:


  • 编写键时出错 :可以使用不同的键来读取和写入同一实体。
  • 值的不固定类型 :例如,使用相同的键,您可以写入数字和读取字符串。
  • 按键冲突 :具有相同按键的不同实体可以记录在项目的不同部分。

在理想情况下,编译器应避免出现这些问题,如果存在键冲突或值类型不匹配,则不允许您构建项目。 为了实现这种安全存储,可以使用容器作为值,本演练将帮助您使用UserDefaults作为示例来准备它们。


存储协议


因此,我们首先需要的是存储库本身的协议,这将有助于从存储库类型中抽象出来。 因此,我们将有一个单一接口,可与UserDefaults ,钥匙串以及其他一些键值存储一起使用。 该协议看起来非常简单:


 protocol KeyValueStorage { func value<T: Codable>(forKey key: String) -> T? func setValue<T: Codable>(_ value: T?, forKey key: String) } 

因此,任何符合KeyValueStorage协议的存储都必须实现两个通用方法 :字符串形式的键值的getter和setter。 同时,值本身对应于Codable协议,该协议允许存储具有通用表示形式的类型的实例(例如JSONPropertyList )。


标准数据仓库实现不支持Codable类型的值,例如,相同的UserDefaults 。 因此,这种类型上的区别是一个附加好处,它使您既可以存储Swift原语(数字,字符串等)又可以存储整个数据结构,同时保持存储接口的简单性。


协议实施


有两种方法可以实现KeyValueStorage协议:


  • 根据协议对现有存储进行签名并添加必要的方法:

 extension UserDefaults: KeyValueStorage { func value<T: Codable>(forKey key: String) -> T? { //   } func setValue<T: Codable>(_ value: T?, forKey key: String) { //   } } 

  • 将存储包装为单独的类型,隐藏其字段以供外部使用:

 class PersistentStorage: KeyValueStorage { private let userDefaults: UserDefaults let suiteName: String? let keyPrefix: String init?(suiteName: String? = nil, keyPrefix: String = "") { guard let userDefaults = UserDefaults(suiteName: suiteName) else { return nil } self.userDefaults = userDefaults self.suiteName = suiteName self.keyPrefix = keyPrefix } func value<T: Codable>(forKey key: String) -> T? { //   } func setValue<T: Codable>(_ value: T?, forKey key: String) { //   } } 

第二种方法看起来更复杂,但是扩展性更好,例如,您可以为每个存储实例设置键前缀。 另外,包装器隐藏额外的字段,仅保留协议字段可用,从而解决了签名冲突的潜在问题。


要自己实现方法value(forKey:)setValue(:forKey:)提供数据兼容性非常重要。 这是必需的,以便可以通过KeyValueStorage的方法检索标准UserDefaults工具存储的值,反之亦然。


在此处使用现成的PersistentStorage类的完整示例。


价值容器


现在,我们已经从存储类型中抽象出来了,我们将通过键为值添加一个容器。 将所有必需字段封装到一个方便的实体中将很有用,该实体可以与存储本身分开传输和使用。 这样的容器被实现为一个小的通用类


 class KeyValueContainer<T: Codable> { let storage: KeyValueStorage let key: String var value: T? { get { storage.value(forKey: key) } set { storage.setValue(newValue, forKey: key) } } init(storage: KeyValueStorage, key: String) { self.storage = storage self.key = key } } 

容器的值类型受Codable协议限制的方式与存储本身的方法相同,因此,计算出的属性value仅将具有固定键和值类型的调用代理给它。


如果需要,可以扩展容器的功能,例如,添加默认值,如果存储中没有密钥的数据,则将使用该默认值。


具有默认值的容器的示例实现
 class KeyValueContainer<T: Codable> { let storage: KeyValueStorage let key: String let defaultValue: T? var value: T? { get { storage.value(forKey: key) ?? defaultValue } set { storage.setValue(newValue, forKey: key) } } init(storage: KeyValueStorage, key: String, defaultValue: T? = nil) { self.storage = storage self.key = key self.defaultValue = defaultValue } } 

容器解决了我们的前两个问题-它为读取和写入的数据确定了密钥和类型。 因此,任何试图写入错误类型值的尝试都将在编译阶段由编译器停止:


 func doSomething(with container: KeyValueContainer<Int>) { container.value = "Text" //   } 

解决最后一个问题仍然是要保证存储密钥的唯一性。 也可以使用相当有趣的技巧将这种担忧挂在编译器上。


关键唯一性


为了解决冲突问题,我们使用将在其中创建容器本身的计算属性的名称作为键。 为此,向我们的KeyValueStorage协议添加一个简单的扩展:


 extension KeyValueStorage { func makeContainer<T: Codable>(key: String = #function) -> KeyValueContainer<T> { KeyValueContainer(storage: self, key: key) } } 

因此,在存储协议的所有实现中,将添加通用方法,该方法返回具有指定键的容器。 此方法特别keykey参数,默认情况下它的值等于特殊表达式#functiondocumentation )。 这意味着在构建阶段,将#function文字替换为makeContainer(key:)调用makeContainer(key:)方法的声明的名称。


这种构造允许您在存储扩展中声明容器,并且如果makeContainer()方法而不包含key参数,则它们的名称将是计算属性的名称:


 extension PersistentStorage { var foobar: KeyValueContainer<Int> { makeContainer() } } 

在该示例中, PersistentStorage存储实例将使用具有相同名称的foobar键的容器接收foobar属性,其值类型将为整数。 尝试添加第二个foobar容器进行存储将导致编译错误,从而保证了键的唯一性。



总结一下


值容器解决了所有提到的存储接口问题,并且不限于UserDefaults 。 在具有相应实现的KeyValueStorage协议下签名(包装)任何键值存储就足够了,并且它已经可以用于创建安全容器。


另外,值得注意的是添加和使用此类容器的便利性,所有内容都限于简单易懂的设计:


 extension PersistentStorage { //   var foobar: KeyValueContainer<Int> { makeContainer() } } //   let foobar = storageInstance.foobar.value //   storageInstance.foobar.value = 123 

您只需要习惯一个事实,即计算属性的名称是该值的键,这在重构代码时尤其重要。 另外,如果您仍然无法重命名,请不要忘记存储库的迁移。


仅此而已。 我很乐意在评论中提供反馈。 再见!

Source: https://habr.com/ru/post/zh-CN481456/


All Articles