Swift Codable 深入解析:理解 CodingKeys 的关键角色
Swift Codable 深入解析:理解 CodingKeys 的关键角色

Swift Codable 深入解析:理解 CodingKeys 的关键角色

问题分析

本篇文章的问题在于深入理解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,我们可以:

  1. 控制哪些属性会被编码和解码。
  2. 自定义属性在编码时使用的键名。

如果不使用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协议要求枚举符合以下条件:

  1. 枚举的每个case必须具有String或Int类型的原始值。
  2. 编译器会利用这些原始值作为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 中定义的键。这确保了:

  1. 在编码时,Swift 会用 CodingKeys 中的键来生成 JSON 字段。
  2. 在解码时,Swift 会使用 JSON 中的这些键来匹配并解析属性值。

CodingKey 协议还要求我们定义枚举的原始值类型(如 String 或 Int),因为编码和解码时需要将这些键映射为具体的字符串或整数键。

CodingKeys和CodingKey总结

enum CodingKeys: String, CodingKey {
    case _name = "name"
}

在我们的CodingKeys代码中:

  1. CodingKeys是Codable的一种规范,表示哪些属性会被编码/解码。
  2. 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 协议,将导致编码和解码无法正确匹配键名,从而引发问题。

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开放者。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注