默认情况下,使用Swift内置的可编码API对数组进行编码或解码是一种全有或全无的交易。要么成功地处理所有元素,要么抛出错误,这无疑是一种良好的默认做法,因为它确保了高度的数据一致性。
然而,有时我们可能想要调整这种行为,以便忽略无效元素,而不是导致整个编码过程失败。例如,假设我们正在使用一个基于json的web API,它返回我们目前在Swift中建模的项目集合,如下所示:
struct Item: Codable {
var name: String
var value: Int
}
extension Item {
struct Collection: Codable {
var items: [Item]
}
}
现在,假设我们正在使用的web API偶尔会返回如下响应,其中包括一个空值,我们的Swift代码期望的是Int:
{
"items": [
{
"name": "One",
"value": 1
},
{
"name": "Two",
"value": 2
},
{
"name": "Three",
"value": null
}
]
}
如果我们试图将上述响应解码为我们Item.Collection的模型实例,那么整个解码过程将失败,即使我们的大多数items确实包含完全有效的数据。
上面的例子可能看起来有点做作,但在野外遇到格式不规范或不一致的JSON格式是非常常见的,而且我们可能不能总是调整这些格式来完全适应Swift的静态特性。
当然,一个可能的解决方案是简单地将我们的value属性设为可选(Int?),但这样做可能会在我们的代码库中引入各种各样的复杂性,因为每次我们希望将这些值作为具体的、非可选的Int值使用时,我们都必须unwrap这些值。
解决这个问题的另一种方法是为我们预期可能为null、missing或invalid的属性定义默认值 -当我们仍然想保留任何包含无效数据的元素时,这将是一个很好的解决方案,但假设这不是这些情况之一。
因此,让我们来看看如何在解码任何可解码数组时忽略所有无效元素,而不必对Swift类型中的数据结构进行任何重大修改。
Building a lossy codable list type
我们真正想做的是改变我们的解码过程,从非常严格变成“有损”。 首先,让我们引入一个通用的LossyCodableList类型,它将作为元素值数组的一个薄包装器:
struct LossyCodableList<Element> {
var elements: [Element]
}
请注意,我们并没有立即使我们的新类型符合可编码,这是因为我们希望它有条件地支持可解码、可编码,或者两者都支持,这取决于与它一起使用的元素类型。毕竟,不是所有类型都可以用两种方式编码,通过分别声明我们的可编码一致性,我们将使新的LossyCodableList类型尽可能灵活。
让我们从可解码的开始,通过使用中间的ElementWrapper类型以可选的方式解码每个元素,我们将遵循可解码的原则。然后,我们将使用compactMap丢弃所有的nil元素,这将给我们最终的数组——像这样:
extension LossyCodableList: Decodable where Element: Decodable {
private struct ElementWrapper: Decodable {
var element: Element?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
element = try? container.decode(Element.self)
}
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let wrappers = try container.decode([ElementWrapper].self)
elements = wrappers.compactMap(\.element)
}
}
要了解更多关于上述符合协议的方法,请查看“Swift中的条件一致性”。
接下来,可编码,这可能不是每个项目都需要的东西,但它仍然可以派上用场,以防我们也想给我们的编码过程相同的有损行为:
extension LossyCodableList: Encodable where Element: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
for element in elements {
try? container.encode(element)
}
}
}
有了上面的内容,我们现在可以通过使用新的LossyCodableList让我们的嵌套集合类型自动丢弃所有无效的项值,就像这样:
extension Item {
struct Collection: Codable {
var items: LossyCodableList<Item>
}
}
Making our list type transparent
然而,上述方法的一个主要缺点是,我们现在总是需要使用items.elements来访问实际的item值,这并不理想。如果我们能够将LossyCodableList的使用转变为一个完全透明的实现细节,这样我们就可以继续以简单的值数组的形式访问items属性,那么情况就会好多了。
实现这一目标的一种方法是将项目集合的LossyCodableList存储为私有属性,然后在编码或解码时使用CodingKeys类型指向该属性。 然后,我们可以将item作为一个计算属性来实现,例如:
extension Item {
struct Collection: Codable {
enum CodingKeys: String, CodingKey {
case _items = "items"
}
var items: [Item] {
get { _items.elements }
set { _items.elements = newValue }
}
private var _items: LossyCodableList<Item>
}
}
另一个选项是为我们的Collection类型提供一个完全自定义的Decodable实现,这将涉及在将结果元素赋值给items属性之前使用LossyCodableList对每个JSON数组进行解码:
extension Item {
struct Collection: Codable {
enum CodingKeys: String, CodingKey {
case items
}
var items: [Item]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let collection = try container.decode(
LossyCodableList<Item>.self,
forKey: .items
)
items = collection.elements
}
}
}
以上两种方法都是非常好的解决方案,但是让我们看看我们是否可以通过使用Swift的属性包装器特性使事情变得更好。
Both a type and a property wrapper
属性包装器在Swift中实现的一件真正整洁的事情是,它们都是标准的Swift类型,这意味着我们可以对lossycodabllist进行改进,使其也能够充当属性包装器。
我们要做的就是用@propertyWrapper属性标记它,并实现所需的wrappedValue属性(这同样可以作为一个计算属性来完成):
@propertyWrapper
struct LossyCodableList<Element> {
var elements: [Element]
var wrappedValue: [Element] {
get { elements }
set { elements = newValue }
}
}
有了上面的内容,我们现在就可以用@LossyCodableList属性标记任何基于数组的属性,并且它将被有损编码和解码——相当透明:
extension Item {
struct Collection: Codable {
@LossyCodableList var items: [Item]
}
}
当然,我们仍然可以继续使用LossyCodableList作为独立类型,就像我们之前所做的那样。 通过将其转换为属性包装器,我们所做的一切也使它能够以这种方式使用,这再次为我们提供了很多额外的灵活性,而没有任何实际成本。
Conclusion
乍一看,Codable似乎是一个极其严格和有限的API,要么成功要么失败,没有任何细微差别或定制的空间。然而,一旦我们超越了表面层面,coable实际上是非常强大的,并且可以通过许多不同的方式进行定制。
悄悄地忽略无效的元素绝对不是总是正确的方法——我们经常希望编码过程在遇到任何无效数据时失败-但如果不是这样,那么本文中使用的任何一种技术都可以提供一种很好的方式,使我们的编码代码更加灵活和有损,而不会引入大量额外的复杂性。