Swift中带Codable的不确定类型

可在此处找到该帖子的配套游乐场。

时光飞逝。 Swift 4.0于2017年9月发布,并且一段时间以来我们一直在享受Codable协议。 但是,我们仍有一些基础。

回想一下, Codable协议使对符合该协议的值进行编码和解码变得非常容易。 哦,它带有属性列表JSON支持。 还记得Swift 4之前无数的JSON解码库吗?

在大多数情况下,当您声明采用Codable的类型时,编译器将完成大部分工作并合成一致性 。 如果JSON键与属性名称不匹配,则可能还需要指定CodingKeys枚举。

但是,在某些情况下有必要手动实现Codable 。 其中的一种就是使用JSON包含对象,这些对象的类型由"type"键的值确定。

考虑一个假设的Messaging API ,它支持各种附件:图像,音频等。

  { 
“ from”:“ Guille”,
“ text”:“看看我刚刚发现的东西!”,
“附件”:[
{
“ type”:“ image”
“有效载荷”:{
“ url”:“ http://via.placeholder.com/640x480”,
“宽度”:640,
“身高”:480
}
},
{
“ type”:“ audio”
“有效载荷”:{
“ title”:“永不放弃你”,
“ url”:“ https://audio.com/NeverGonnaGiveYouUp.mp3”,
“ shouldAutoplay”:是的,
}
}
]
}

由于Swift是一种强类型语言,因此我们必须为每种附件实现一种类型。

  struct ImageAttachment :可编码{ 
让网址:URL
让宽度:整数
高度:Int
} struct AudioAttachment :可编码{
让标题:字符串
让网址:URL
让我们玩一下:布尔
}

如您所见,这很简单,直到必须实现Message类型。

  struct讯息:可编码{ 
从:字符串
让文字:字符串
让附件: [???]
}

要完成Message的实现,我们必须首先创建一个可以容纳ImageAttachmentAudioAttachment值的Attachment类型。

有几种方法可以做到这一点。 我们将探索两种不同的方法 ,每种方法各有利弊。

Swift具有适合这种情况的语言功能。 枚举可以存储任何给定类型的关联值。

 枚举附件{ 
案例图片( ImageAttachment
案例音频( AudioAttachment
不支持的情况
}

请注意,如果我们的Messaging API决定支持我们不知道如何处理的新附件类型,我们将添加unsupported

在这种情况下,编译器将无法合成与Codable一致性,但是自己完成它并不困难。

对于Decodable部分,我们必须创建一个CodingKeys枚举并实现init(from: Decoder)

 扩展附件:可编码{ 
私有枚举CodingKeys:字符串,CodingKey {
案例类型
案件有效载荷
} init(来自解码器:Decoder)抛出{
让容器=尝试解码器。容器(keyedBy:CodingKeys.self)
让type = try container.decode(String.self,forKey:.type)开关类型{
案例“图片”:
让有效负载=尝试container.decode(ImageAttachment.self,forKey:.payload)
自我= .image(有效载荷)
案例“音频”:
让有效负载=尝试container.decode(AudioAttachment.self,forKey:.payload)
自我= .audio(有效载荷)
默认:
自我= .unsupported
}
}
...
}

对于Encodable部分,我们必须通过打开self并让关联的值对其进行编码来实现encode(to: Encoder)

  func encode(编码器:编码器)抛出{ 
var container = encoder.container(keyedBy:CodingKeys.self)switch self {
大小写.image(让附件):
尝试container.encode(“ image”,forKey:.type)
尝试container.encode(attachment,forKey:.payload)
案例.audio(让附件):
尝试container.encode(“ audio”,forKey:.type)
尝试container.encode(attachment,forKey:.payload)
案例.unsupported:
let context = EncodingError.Context(codingPath:[],debugDescription:“无效的附件。”)
抛出EncodingError.invalidValue(self,context)
}
}

最后,我们需要在Message指定attachments类型。

  struct讯息:可编码{ 
从:字符串
让文字:字符串
让附件:[附件]
}

检查任何邮件的附件是一个简单的任务。

 让解码器= JSONDecoder() 
让message =试试解码器.decode(Message.self,来自:json)获取message.attachments中的附件{
开关附件{
案例.image(let image):
//对图片进行处理
案例.audio(让音频):
//对音频做一些事情
案例.unsupported:
//忽略不支持的附件
}
}

为了支持新型附件,我们需要执行以下任务:

  1. 为新附件实现类型。
  2. 将一个新案例添加到Attachment枚举并更新其Codable实现。

第二点是此实现的主要缺点所在。 开放/封闭原则指出:

软件实体应为扩展而开放,但应为修改而封闭。

我们应该找到一种实现Attachment的方法,以便在我们必须支持一种新型的附件时不需要修改它

如果我们想避免以后进行任何修改,我们别无选择,只能使用Any来存储附件。

 结构附件{ 
让类型:字符串
让有效载荷:有吗?

私有枚举CodingKeys:字符串,CodingKey {
案例类型
案件有效载荷
}
...
}

可能看起来像是退后一步,但请忍受,还不错。

Codable协议不适用于Any 。 我们需要一种注册附件类型的方法。

  Attachment.register(ImageAttachment.self,用于:“图像”) 
Attachment.register(AudioAttachment.self,用于:“音频”)

register的实现应统一存储逻辑,以对给定的附件类型进行解码和编码,并以JSON中的类型名称作为关键字。 回想一下我们如何解码附件:

 尝试container.decode(ImageAttachment.self,forKey:.payload) 

我们如何将这种逻辑统一存储在Dictionary

为了统一存储解码和编码逻辑,我们必须擦除类型信息。 让我们为编码和解码闭包定义签名。

  typealias AttachmentDecoder =(KeyedDecodingContainer )引发->任何 
typealias AttachmentEncoder =(任何,inout KeyedEncodingContainer )throws-> Void

AttachmentDecoder闭包采用一个容器并返回对其内容进行解码的结果。

AttachmentEncoder闭包采用附件有效负载并使用给定的容器对其进行编码。

现在我们需要两个私有的静态属性来分别存储解码器和编码器。

 私有静态var 解码器 :[String:AttachmentDecoder] = [:] 
私有静态var 编码器 :[String:AttachmentEncoder] = [:]

最后,我们可以实现我们的register方法来存储给定附件类型的解码和编码闭包。

 静态函数寄存器 (_类型:A.Type,用于typeName:String){ 
解码器[typeName] = {
尝试container.decode(A.self,forKey:.payload)
} coders [typeName] = {有效负载,容器位于
尝试container.encode(payload as!A,forKey:.payload)
}
}

现在我们有了一种注册附件类型的方法,实现Codable就像依赖decodersencoders静态属性一样容易。

要解码Attachment ,我们访问type属性的值,并使用它来找到对应的解码器。 然后,我们使用该解码器解码有效负载。

  init(来自解码器:解码器)抛出{ 
让容器=尝试解码器。容器(keyedBy:CodingKeys.self)
类型=尝试让container.decode(String.self,forKey:.type)(如果让解码= Attachment.decoders [type] {
有效负载=尝试解码(容器)
}其他{
有效载荷=零
}
}

为了对Attachment进行编码,我们首先对其Attachment进行编码,然后找到相应的编码器,然后将其用于对有效负载进行编码。

  func encode(编码器:编码器)抛出{ 
var container = encoder.container(keyedBy:CodingKeys.self)如果让载荷= self.payload,请尝试container.encode(type,forKey:.type)
保护卫队编码= Attachment.encoders [type]其他{
let context = EncodingError.Context(codingPath:[],debugDescription:“无效的附件:\(类型)。”)
抛出EncodingError.invalidValue(self,context)
}尝试编码(有效载荷和容器)
}其他{
尝试container.encodeNil(forKey:.payload)
}
}

这可能会让您感到惊讶,但是检查邮件的附件与我们使用枚举解决方案的方式并没有很大不同。

在进行任何解码或编码之前,请注册支持的附件,这一点很重要。

  Attachment.register(ImageAttachment.self,用于:“图像”) 
Attachment.register(AudioAttachment.self,用于:“音频”)

由于我们可以使用switch语句进行类型转换,因此其余部分大致相同。

 让解码器= JSONDecoder() 
让message =试试解码器.decode(Message.self,来自:json)获取message.attachments中的附件{
切换attachment.payload {
案例让图像作为ImageAttachment:
//对图片进行处理
案例让音频作为AudioAttachment:
//对音频做一些事情
默认:
//忽略不支持的附件
}
}

要支持新附件,我们只需为附件实现新类型并注册即可 。 无需修改Attachment的实现。

带有关联值的枚举是我最喜欢的Swift语言功能之一。 但是,我认为它们不适合此用例。

Attachment有效负载使用Any并为附件类型实现注册机制可以在保持相似开发经验的同时进行修改。

可在此处找到该帖子的配套游乐场。