我正在尝试使用Swift 4中新的JSONDecoder/Encoder来找到对符合Swift协议的结构体数组进行编码/解码的最佳方法。
我编了一个小例子来说明这个问题:
首先,我们有一个协议标签和一些符合该协议的类型。
protocol Tag: Codable {
var type: String { get }
var value: String { get }
}
struct AuthorTag: Tag {
let type = "author"
let value: String
}
struct GenreTag: Tag {
let type = "genre"
let value: String
}
然后我们有一个Type Article,它有一个标签数组。
struct Article: Codable {
let tags: [Tag]
let title: String
}
最后对文章进行编码或解码
let article = Article(tags: [AuthorTag(value: "Author Tag Value"), GenreTag(value:"Genre Tag Value")], title: "Article Title")
let jsonEncoder = JSONEncoder()
let jsonData = try jsonEncoder.encode(article)
let jsonString = String(data: jsonData, encoding: .utf8)
这是我喜欢的JSON结构。
{
"title": "Article Title",
"tags": [
{
"type": "author",
"value": "Author Tag Value"
},
{
"type": "genre",
"value": "Genre Tag Value"
}
]
}
问题是,在某些时候,我必须打开type属性来解码数组,但要解码数组,我必须知道它的类型。
编辑:
我很清楚为什么Decodable不能开箱即用,但至少Encodable应该可以工作。下面修改的文章结构编译,但崩溃与以下错误信息。
fatal error: Array<Tag> does not conform to Encodable because Tag does not conform to Encodable.: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-900.0.43/src/swift/stdlib/public/core/Codable.swift, line 3280
struct Article: Encodable {
let tags: [Tag]
let title: String
enum CodingKeys: String, CodingKey {
case tags
case title
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(tags, forKey: .tags)
try container.encode(title, forKey: .title)
}
}
let article = Article(tags: [AuthorTag(value: "Author Tag"), GenreTag(value:"A Genre Tag")], title: "A Title")
let jsonEncoder = JSONEncoder()
let jsonData = try jsonEncoder.encode(article)
let jsonString = String(data: jsonData, encoding: .utf8)
这是Codeable.swift中的相关部分
guard Element.self is Encodable.Type else {
preconditionFailure("\(type(of: self)) does not conform to Encodable because \(Element.self) does not conform to Encodable.")
}
来源:https://github.com/apple/swift/blob/master/stdlib/public/core/Codable.swift
5条答案
按热度按时间bfnvny8b1#
第一个示例无法编译(第二个示例崩溃)的原因是protocols don't conform to themselves-
Tag
不是符合Codable
的类型,因此[Tag]
也不是。因此Article
没有自动生成Codable
一致性,因为它的属性并不都符合Codable
。只对协议中列出的属性进行编码和解码
如果您只想对协议中列出的属性进行编码和解码,一种解决方案是使用
AnyTag
类型擦除器,它只保存这些属性,然后可以提供Codable
一致性。然后,您可以让
Article
保存此类型擦除 Package 器的数组,而不是Tag
的数组:它输出以下JSON字符串:
并且可以如下解码:
对符合类型的所有属性进行编码和解码
但是,如果需要对给定
Tag
一致类型的 every 属性进行编码和解码,则可能希望以某种方式将类型信息存储在JSON中。我将使用
enum
来执行此操作:这比仅仅使用普通字符串来表示类型要好,因为编译器可以检查我们是否为每种情况提供了一个元类型。
然后,您只需要更改
Tag
协议,使其要求一致类型实现描述其类型的static
属性:然后,我们需要调整类型擦除 Package 器的实现,以便对
TagType
沿着基Tag
进行编码和解码:我们使用了一个超级编码器/解码器,以确保给定的一致类型的属性键不会与用于编码该类型的键冲突。例如,编码后的JSON将如下所示:
但是,如果您知道不会有冲突,并且希望属性在与“type”键 * 相同 * 的级别上进行编码/解码,则JSON如下所示:
在上面的代码中,可以传递
decoder
而不是container.superDecoder(forKey: .base)
,也可以传递encoder
而不是container.superEncoder(forKey: .base)
。作为一个 * 可选 * 的步骤,我们可以定制
Article
的Codable
实现,这样我们就可以提供自己的实现,在编码之前将[Tag]
装箱到[AnyTag]
中,然后解装箱以进行解码,而不是依赖于自动生成的与[AnyTag]
类型的tags
属性的一致性:这样我们就可以让
tags
属性的类型为[Tag]
,而不是[AnyTag]
。现在,我们可以对
TagType
枚举中列出的任何Tag
兼容类型进行编码和解码:输出JSON字符串:
然后可以如下解码:
ubof19bj2#
受到@Hamish回答的启发。我发现他的方法是合理的,但有几点可以改进:
1.将数组
[Tag]
Map到Article
中的[AnyTag]
,以及从Article
Map数组[Tag]
,使我们无法自动生成Codable
一致性1.基类的数组编码不可能使用相同的代码,因为
static var type
不能在子类中被覆盖。(例如,如果Tag
是AuthorTag
和GenreTag
的超类)1.最重要的是,此代码不能重用于另一个类型,您需要创建新的AnyAnotherType Package 器及其内部编码。
我提出了稍微不同解决方案,不是 Package 数组的每个元素,而是可以对整个数组进行 Package :
其中
Meta
是通用协议:现在,存储标记将如下所示:
结果JSON:
如果您希望JSON看起来更漂亮:
添加到
Meta
协议并将
CodingKeys
替换为:ncecgwcz3#
从这个被接受的答案中,我得到了下面的代码,可以粘贴到Xcode Playground中。
输出如下所示,* 没有 * 已接受答案中提到的嵌套。
粘贴到您的Xcode项目或Playground中,并根据您的喜好自定:
zu0ti5jz4#
为什么不对标记的类型使用枚举?
然后你可以像
try? JSONEncoder().encode(tag)
那样编码或者像let tags = try? JSONDecoder().decode([Tag].self, from: jsonData)
那样解码,并且做任何类型的处理,比如按类型过滤标签。你也可以对Article结构体做同样的处理:uqdfh47h5#
我从@Hamish那里得到了一个公认的答案,这个答案很棒,我把它概括了一下。也许对其他人有用,所以把它贴在这里...
首先,设置类似于
AnyTag
和TagType
的可重用类型。现在让你的具体类型利用这些。
最后,对类型进行编码/解码。
我对
Fleet
的解码方法中使用的转换不是很满意,但是试图通过改变泛型来避免这种情况的尝试遇到了经典的错误,比如“任何车辆都不能符合车辆”。如果有人能找到更好的方法,我很乐意听到它。