问题分析
本篇文章的问题在于深入理解Adding Codable conformance to an @Observable class文章中涉及CodingKeys、CodingKey等知识。
在Swift中,如果类型的所有属性都符合Codable,则类型本身无绪额外工作即可符合条件。但是,由于Swift重写代码的方式,Codable使用宏的类(@Observable)时,事情会变得复杂一些。
@Observable
class User: Codable {
var name = "Taylor"
}
struct CupcakeCorner: View {
var body: some View {
Button("Encode Taylor", action: encodeTaylor)
}
func encodeTaylor() {
let data = try! JSONEncoder().encode(User())
let str = String(decoding: data, as: UTF8.self)
print(str)
}
}
在上述代码当中,点击Button按钮,Xcode输出:
{"_name":"Taylor","_$observationRegistrar":{}}
这是因为 @Observable 宏改变了User类,在幕后进行一些重写从而支持 SwiftUI 观察属性的机制。这种重写有时会导致与 Codable 协议的默认行为冲突。
@Observable的重写行为
当我们使用 @Observable 宏时,Swift 会对类进行一些隐式的修改。具体来说,它会生成额外的代码,以便让 SwiftUI 能够监控对象的属性变化。这包括创建额外的存储来跟踪属性,甚至可能重命名我们的属性以符合其内部使用的格式。
在上面的示例中,name 属性被改成了 _name,同时还增加了一个 _$observationRegistrar 属性。这是 @Observable 工作机制的一部分,用于管理观察者的注册。
因为我们使用JSONEncoder对象及逆袭编码,它会直接读取类的属性名。这意味着如果@Observable把 name 属性重新命名为 _name,编码的结果就会用 _name 作为键。
此外,还有额外的 _$observationRegistrar 属性会出现在编码结果中,这都不是我们想要看到的。
解决方案
通过定义CodingKeys枚举,我们可以指定哪些属性应该被编码,以及它们在JSON中使用什么键,以便将 _name 改回name,并排查不希望被编码的属性(如 _$observationRegistrar)。
@Observable
class User: Codable {
enum CodingKeys: String, CodingKey {
case _name = "name"
}
var name = "Taylor"
}
在这段代码中,我们新增了一个 enum CodingKeys枚举:
case _name = “name”解决了 _name 属性问题,尽管属性在类中被@Observable改名为 _name,但是Codable会按照CodingKeys的映射规则,将它保存为name。这样的映射,编码和解码操作可以正确处理name,同时不必暴露@Observable生成的额外信息。
当我们再次点击按钮,我们就可以正常输出:
{"name":"Taylor"}
什么是CodingKeys?
CodingKeys 是一个约定名称,通常用于实现 Codable 协议。当我们手动实现 Codable 时,Swift 会寻找名为 CodingKeys 的枚举,以便知道在编码和解码时应该使用哪些属性和对应的键名。
CodingKeys 枚举中的每个 case 对应类中的一个属性。通过定义这些 case,我们可以:
- 控制哪些属性会被编码和解码。
- 自定义属性在编码时使用的键名。
如果不使用CodingKeys枚举
如果我们在实现Codable协议时没有定义CodingKeys枚举,有以下两种情况:
1、自动生成
自动生成的CodingKeys是一个Swift编译器隐式创建的枚举,它遵循CodingKey协议。
这个枚举包含一个cas对应于每个类或结构体中的属性,case的名称与属性名称相同。
在编码和解码的过程中,Swift会使用CodingKeys枚举来匹配类/结构体的属性和JSON字段。
如果我们的类的所有属性都符合 Codable 协议,并且没有做任何特别的处理(如属性名称映射),Swift 会自动为我们的类型生成默认的编码和解码实现。这意味着它会直接按照属性名称来进行编码和解码。因此,我们的类会被编码成一个 JSON 对象,属性名直接使用类中的变量名。例如:
class User: Codable {
var name = "Taylor"
var age = 25
}
在这种情况下,Swift 会自动生成以下等价的 CodingKeys枚举,并将 name 和 age 作为 JSON 字段的键,这样会得到:
enum CodingKeys: String, CodingKey {
case name
case age
}
Swift编译器利用这个CodingKeys枚举自动将name和age作为JSON的键,因此编码和解码的JSON会是:
{
"name": "Taylor",
"age": 25
}
所以,即使没有显式定义CodingKeys,Swift会根据类的属性名称生成一个,以确保编解码过程能够顺利完成。
2、无法控制键名
在本次样例代码中。因为@Observable宏的重写行为,导致JSON中使用不同于类中属性名称的键名(例如把name编码为 _name)。
@Observable
class User: Codable {
var name = "Taylor"
var age = 25
}
因为我们的User类使用的是@Observable宏,所以Swift会因为@Observable在编译时重写类的属性,使这些属性变为私有存储属性,例如 _name和 _age,这些都是@Observable宏进行的幕后操作,它会添加额外的属性来支持观察功能,从而导致属性名称的变化。
具体来讲:
@Observable重写了name和age,使它们存储在私有变量 _name和 _age中。
编译器自动生成的CodingKeys枚举会基于这些重写后的名称,所以会包括 _name和 _age。
_$observationRegistrar是@Observable添加的,用于支持观察功能的内部属性,因此也会包含在内。
生成下面的CodingKeys枚举:
enum CodingKeys: String, CodingKey {
case _name
case _age
case _$observationRegistrar
}
因此,如果我们希望控制最终的JSON键名,就需要手动定义CodingKeys,将name和age映射回去:
enum CodingKeys: String, CodingKey {
case _name = "name"
case _age = "age"
}
如果CodingKeys不遵守CodingKey协议
如果我们定义了CodingKeys枚举,但是没有让它遵守CodingKey协议,那么编译器就会报错。
Type 'User' does not conform to protocol 'Decodable'
Type 'User' does not conform to protocol 'Encodable'
这是因为CodingKey协议要求枚举具有某些特性,使得它可以用于编码和解码过程。具体来说,CodingKey协议要求枚举符合以下条件:
- 枚举的每个case必须具有String或Int类型的原始值。
- 编译器会利用这些原始值作为JSON编码/解码时的字段键。
如果我们定义了CodingKeys,但是没有遵守CodingKey,编译器就不知道如何将我们的枚举case映射为编码时的字段键,就会导致编译失败的问题。
更换CodingKeys名称
当我们尝试将CodingKeys改名为CodingK或者其他名称时:
enum CodingK:String, CodingKey {
case _name = "name"
}
我们的CodingKeys枚举就会失效。
在实现Codable协议时,CodingKeys枚举的名称不能随意更改。Swift使用这个特定名称作为约定来查找定义的键映射,如果尝试将它换成其他名称,编译器就无法识别它,并且不会应用我们自定义的编码/解码逻辑。
什么是CodingKey
CodingKey 是一个协议,专门用于定义编码和解码键。它的作用是让枚举中的每个 case 都可以被当作一个键来使用。
当 CodingKeys 枚举符合 CodingKey 协议后,它会告诉 JSONEncoder 和 JSONDecoder 在处理这个类的编码和解码时,需要使用 CodingKeys 中定义的键。这确保了:
- 在编码时,Swift 会用 CodingKeys 中的键来生成 JSON 字段。
- 在解码时,Swift 会使用 JSON 中的这些键来匹配并解析属性值。
CodingKey 协议还要求我们定义枚举的原始值类型(如 String 或 Int),因为编码和解码时需要将这些键映射为具体的字符串或整数键。
CodingKeys和CodingKey总结
enum CodingKeys: String, CodingKey {
case _name = "name"
}
在我们的CodingKeys代码中:
- CodingKeys是Codable的一种规范,表示哪些属性会被编码/解码。
- CodingKey协议确保每个case都是一个有效的键,允许我们指定属性在JSON中的键名。
通过使用CodingKeys,我们可以控制编码和解码的行为,避免 @Observable 宏对类内部结构的影响,从而得到符合我们预期的 JSON 结果。
总结
默认情况下,Swift会根据属性名直接生成键。则意味着编码和解码时,JSON中的键名将与类或结构体的属性名完全匹配,但在某些情况下(Class使用@Observable宏时),数据模型的属性名和外部数据(例如JSON数据)的键名不同,需要手动控制。
class User: Codable {
var firstName: String
var lastName: String
enum CodingKeys: String, CodingKey {
case firstName = "first_name"
case lastName = "last_name"
}
}
在这个例子中,即使属性名称是 firstName 和 lastName,最终编码出来的 JSON 会是:
{
"first_name": "John",
"last_name": "Doe"
}
如果我们有一些属性不想要包含在编码和解码过程中,我们就可以在CodingKeys枚举中不声明这些数据,只有在CodingKeys中列出的属性才会被编码和解码。这样,我们就可以更好的控制数据的序列化和反序列化。
class User: Codable {
var name: String
var password: String // 不想包含在JSON中的属性
enum CodingKeys: String, CodingKey {
case name
}
}
在处理@Observablie或其他修改器的兼容时,它们可能会自动修改属性名称(例如添加 _),导致属性名和实际编码的键名不一致。因此需要使用CodingKeys强制Codable使用正确的键,即使属性的实际存储名不同。也可以避免错误,并确保编码和解码的一致性。
遵守CodingKey协议,CodingKeys枚举必须符合CodingKey协议,因为 CodingKey 协议定义了编解码过程中使用的键需要满足的一些要求,如转换为字符串和整数。CodingKey 协议确保 Swift 编译器可以在序列化和反序列化过程中使用这些键来访问数据。
最后,CodingKeys 的存在允许开发者更精确地控制编码和解码过程,确保数据模型的属性可以正确映射到外部数据的键。没有 CodingKeys 或者没有正确遵守 CodingKey 协议,将导致编码和解码无法正确匹配键名,从而引发问题。