我已经为项目中的某些类实现了自定义 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 对中为每个成员进行解码尝试......非常乏味。无论如何,结果都会使班级成员为零。
前言:我强烈警告不要提供这样的默认值。如果你最终得到像
User(username: "", firstName: "", lastName: "", EMail: "", phoneNbr: "", ...)
这样的物体,那有什么安全的呢?这些辅助函数已经悄悄地从裂缝中溜走,这是无稽之谈。
尽管如此,我会更笼统地回答这个问题,以展示如何在 Swift 中共享这样的代码。
// 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
}
}
}
}
您可以将这些助手作为方法转储到所有使用它们的模型上,而只需将它们更改为扩展即可
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)
}
}
这是我最喜欢的方法,因为它可以完全消除定义自定义
init(from:)
初始化程序的需要!
查看
Default
包中的 MetaCodable
宏,以供参考: https://github.com/SwiftyLab/MetaCodable/blob/a6011c3337f573b04b29b8591b507de7e6e4ed8d/Sources/MetaCodable/Default.swift