如何将 CodingKeys 作为数组而不是枚举传递到类外部的函数中?

问题描述 投票:0回答:1

我已经为项目中的某些类实现了自定义 JSON 解码。我已经设置了常用的 CodingKeys 枚举并实现了自定义编码和解码函数。这一切都很好。

但我还为解码过程实现了一些方便的函数,它们捕获 JSONDecoder 的 KeyedDecodingContainer 抛出的任何错误并返回正常的内容。因为这些函数不需要在类之间有所不同,所以我想将它们移出类,但大多数示例将编码键定义为类中的硬编码枚举。

这是要解码的类:

class User : Equatable, Codable
{
    var ID: String = ""
    var pw: String = ""
    var username:  String = ""
    var firstName: String = ""
    var lastName: String = ""
    var EMail: String = ""
    var phoneNbr: String = ""
    var avatarURL: String = ""
    var mediaServiceID: String = ""
    var validated: Bool = false // E-mail is confirmed.

    // Equatable
    static func == (lhs: User, rhs: User) -> Bool
    {
        return lhs.ID == rhs.ID
    }

    // Codable
    enum CodingKeys: String, CodingKey
    {
        case ID = "ID"
        case pw = "pw"
        case username = "username"
        case firstName = "firstName"
        case lastName = "lastName"
        case EMail = "EMail"
        case phoneNbr = "phoneNbr"
        case avatarURL = "avatarURL"
        case mediaServiceID = "mediaServiceID"
        case validated = "validated"
    }

    required init(from decoder: Decoder) throws
    {
        var container: KeyedDecodingContainer<CodingKeys>
        do
        {
            container = try decoder.container(keyedBy: CodingKeys.self)
        }
        
        ID =                safeStringDecode(container: container, forKey: .ID)
        pw =                safeStringDecode(container: container, forKey: .pw)
        username =          safeStringDecode(container: container, forKey: .username)
        firstName =         safeStringDecode(container: container, forKey: .firstName)
        lastName =          safeStringDecode(container: container, forKey: .lastName)
        EMail =             safeStringDecode(container: container, forKey: .EMail)
        phoneNbr =          safeStringDecode(container: container, forKey: .phoneNbr)
        avatarURL =         safeStringDecode(container: container, forKey: .avatarURL)
        mediaServiceID =    safeStringDecode(container: container, forKey: .mediaServiceID)
        validated =         safeBoolDecode(container: container, forKey: .validated)
    }

    func safeStringDecode(container: KeyedDecodingContainer<CodingKeys>, forKey: CodingKeys) -> String
    {
        var result: String
        do
        {
            result = try container.decode(String.self, forKey: forKey)
        }
        catch
        {
            result = ""
        }
        
        return result
    }
    
    func safeBoolDecode(container: KeyedDecodingContainer<CodingKeys>, forKey: CodingKeys) -> Bool
    {
        // First try int, because that's what MySQL/MariaDB return for bools.
        var result: Int
        do
        {
            result = try container.decode(Int.self, forKey: forKey)
            return result == 1
        }
        catch
        {
            // Let's see if it's a 'true'/'false' string.
            var stringResult: String
            do
            {
                stringResult = try container.decode(String.self, forKey: forKey)
                return stringResult == "true"
            }
            catch
            {
                return false
            }
        }
    }

我想将这些 safeDecode 方法移出此类并移至通用实用程序命名空间中。但这意味着我无法将编码键硬编码为类中的枚举,因此我需要将它们作为数组或字典传递。看起来应该很简单,但到目前为止我还没有看到简洁的解决方案。这篇文章中的 #3 看起来很接近,但我不太理解其中的一些逻辑:How do I use custom key with Swift 4's Decodable protocol?

“safeDecode”函数的基本原理是,如果任何值丢失或为零,则解码对象将完全失败,这是完全可能的,因为这些列在我的数据库中可以为空。为了防止在没有便利功能的情况下发生这种情况,我必须在单独的 do/catch 对中为每个成员进行解码尝试......非常乏味。无论如何,结果都会使班级成员为零。

swift codable
1个回答
1
投票

前言:我强烈警告不要提供这样的默认值。如果你最终得到像

User(username: "", firstName: "", lastName: "", EMail: "", phoneNbr: "", ...)
这样的物体,那有什么安全的呢?这些辅助函数已经悄悄地从裂缝中溜走,这是无稽之谈。

尽管如此,我会更笼统地回答这个问题,以展示如何在 Swift 中共享这样的代码。

方法 1:将其与协议扩展混合

// An empty marker protocol, which requires the conforming type to
// be decodable
protocol DecodingHelpers: Decodable {
  associatedtype CodingKeys: CodingKey
}

// Mark `User` with the protocol
extension User: DecodingHelpers {}

// Extend all `DecodingHelpers`-conforming types with the helper functions
extension DecodingHelpers {
  func safeStringDecode(container: KeyedDecodingContainer<CodingKeys>, forKey: CodingKeys) -> String {
    (try? container.decode(String.self, forKey: forKey)) ?? ""
  }
  
  func safeBoolDecode(container: KeyedDecodingContainer<CodingKeys>, forKey: CodingKeys) -> Bool {
    do {
      // First try int, because that's what MySQL/MariaDB return for bools.
      return try container.decode(Int.self, forKey: forKey) == 1
    }
    catch {
      do {
        // Let's see if it's a 'true'/'false' string.
        return try container.decode(String.self, forKey: forKey) == "true"
      }
      catch {
        return false
      }
    }
  }
}

方法 2:将它们移至扩展程序

您可以将这些助手作为方法转储到所有使用它们的模型上,而只需将它们更改为扩展即可

KeyedDecodingContainer
:

extension KeyedDecodingContainer {
  func safeStringDecode(forKey: K) -> String {
    (try? decode(String.self, forKey: forKey)) ?? ""
  }
  
  func safeBoolDecode(forKey: K) -> Bool {
    do {
      // First try int, because that's what MySQL/MariaDB return for bools.
      return try decode(Int.self, forKey: forKey) == 1
    }
    catch {
      do {
        // Let's see if it's a 'true'/'false' string.
        return try decode(String.self, forKey: forKey) == "true"
      }
      catch {
        return false
      }
    }
  }
}

// Usage:
class User: Equatable, Codable
  // ...

  required init(from decoder: Decoder) throws {
    let container =  try decoder.container(keyedBy: CodingKeys.self)
    
    ID             = container.safeStringDecode(forKey: .ID)
    pw             = container.safeStringDecode(forKey: .pw)
    username       = container.safeStringDecode(forKey: .username)
    firstName      = container.safeStringDecode(forKey: .firstName)
    lastName       = container.safeStringDecode(forKey: .lastName)
    EMail          = container.safeStringDecode(forKey: .EMail)
    phoneNbr       = container.safeStringDecode(forKey: .phoneNbr)
    avatarURL      = container.safeStringDecode(forKey: .avatarURL)
    mediaServiceID = container.safeStringDecode(forKey: .mediaServiceID)
    validated      = container.safeBoolDecode(forKey: .validated)
  }
}

方法 3:使用属性包装器

这是我最喜欢的方法,因为它可以完全消除定义自定义

init(from:)
初始化程序的需要!

查看

Default
包中的
MetaCodable
宏,以供参考: https://github.com/SwiftyLab/MetaCodable/blob/a6011c3337f573b04b29b8591b507de7e6e4ed8d/Sources/MetaCodable/Default.swift

© www.soinside.com 2019 - 2024. All rights reserved.