《Swift进阶》第十四章(编码和解码)知识点梳理、重点与难点总结

5 阅读10分钟

一、核心知识点罗列

(一)编码和解码的核心协议:Codable

  1. 协议组成
    • Codable是复合协议,由Encodable(编码)和Decodable(解码)组合而成,遵循Codable即同时支持编码与解码。
    • 核心方法:
      • Encodablefunc encode(to encoder: Encoder) throws,将实例编码为外部格式(如JSON、PropertyList)。
      • Decodableinit(from decoder: Decoder) throws,从外部格式解码为实例。
  2. 自动遵循协议的条件
    • 结构体/枚举/类的所有存储属性均遵循Codable,编译器会自动合成EncodableDecodable的实现,无需手动编写。
    • 支持的基础类型:IntStringBool等标准库类型,以及ArrayDictionarySet等集合类型(元素需遵循Codable)。
    • 枚举特殊要求:带关联值的枚举需关联值遵循Codable;无关联值的枚举默认自动遵循。

(二)编码(Encoding)流程

  1. 核心逻辑
    • 通过JSONEncoder(JSON格式)或PropertyListEncoder(属性列表格式)等编码器,调用实例的encode(to:)方法,将实例转换为外部数据格式。
    • 编码步骤:
      1. 编码器创建编码容器(键值容器、无键容器、单值容器)。
      2. 实例将自身属性写入对应容器。
      3. 编码器将容器数据转换为目标格式(如JSON数据)。
  2. 常用编码器配置
    • outputFormatting:设置输出格式(如JSON的缩进、排序键)。
    • keyEncodingStrategy:配置键的编码策略(如蛇形命名、自定义键映射)。
    • dateEncodingStrategy:设置日期类型的编码方式(如时间戳、ISO8601字符串)。

(三)解码(Decoding)流程

  1. 核心逻辑
    • 通过JSONDecoderPropertyListDecoder等解码器,读取外部数据,调用实例的init(from:)初始化方法,重建实例。
    • 解码步骤:
      1. 解码器从外部数据中创建解码容器。
      2. 实例从容器中读取属性值并初始化。
      3. 处理可选值、默认值等特殊场景(如属性缺失时的容错)。
  2. 常用解码器配置
    • 与编码器对应,支持keyDecodingStrategy(键解码策略)、dateDecodingStrategy(日期解码策略)等,需与编码配置一致。

(四)编码容器与数据写入/读取

  1. 容器类型
    • 键值容器(KeyedEncodingContainer/KeyedDecodingContainer):适用于结构体、类等带命名属性的类型,通过CodingKeys映射属性与外部键名。
    • 无键容器(UnkeyedEncodingContainer/UnkeyedDecodingContainer):适用于数组等有序集合类型,按顺序写入/读取元素。
    • 单值容器(SingleValueEncodingContainer/SingleValueDecodingContainer):适用于仅含单个值的类型(如包装类型struct Wrapper<T: Codable> { let value: T })。
  2. 容器操作核心方法
    • 写入:encode(_:forKey:)(键值容器)、encode(_:)(无键/单值容器)。
    • 读取:decode(_:forKey:)(键值容器)、decode(_:)(无键/单值容器)。
    • 可选值处理:decodeIfPresent(_:forKey:),属性缺失时返回nil,避免解码失败。

(五)CodingKeys:键映射与自定义

  1. 核心作用
    • 显式指定属性与外部数据键名的映射关系,解决“属性名与外部键名不一致”问题(如Swift属性用驼峰式,JSON用蛇形命名)。
  2. 语法与规则
    • 定义嵌套枚举enum CodingKeys: String, CodingKey,case名对应属性名,rawValue对应外部键名。
    • 未包含在CodingKeys中的属性,默认不参与编码和解码(需手动处理或添加 CodingKeyscase)。
    • 示例:
      struct Person: Codable {
          let userName: String
          let userAge: Int
          
          enum CodingKeys: String, CodingKey {
              case userName = "user_name"
              case userAge = "user_age"
          }
      }
      

(六)自定义编码和解码实现

  1. 手动实现场景
    • 属性名与外部键名无法通过CodingKeys简单映射(如动态键名)。
    • 需要自定义数据转换(如将JSON字符串转换为自定义枚举)。
    • 处理复杂嵌套结构、默认值填充、数据校验等场景。
  2. 手动实现示例
    • 自定义编码:
      struct Product: Codable {
          let id: String
          let price: Double
          let discountPrice: Double?
          
          func encode(to encoder: Encoder) throws {
              var container = encoder.container(keyedBy: CodingKeys.self)
              try container.encode(id, forKey: .id)
              try container.encode(price, forKey: .price)
              // 自定义逻辑:折扣价为nil时,编码为原价的8折
              let finalDiscountPrice = discountPrice ?? price * 0.8
              try container.encode(finalDiscountPrice, forKey: .discountPrice)
          }
          
          enum CodingKeys: String, CodingKey {
              case id, price, discountPrice = "discount_price"
          }
      }
      
    • 自定义解码:
      init(from decoder: Decoder) throws {
          let container = try decoder.container(keyedBy: CodingKeys.self)
          id = try container.decode(String.self, forKey: .id)
          price = try container.decode(Double.self, forKey: .price)
          // 容错逻辑:折扣价小于0时,设为nil
          discountPrice = try container.decodeIfPresent(Double.self, forKey: .discountPrice)
          if let discount = discountPrice, discount < 0 {
              discountPrice = nil
          }
      }
      

(七)枚举的编码和解码

  1. 无关联值枚举
    • 自动遵循Codable,默认编码为原始值(需指定原始值类型,如enum Status: String, Codable { case active, inactive })。
  2. 带关联值枚举
    • 关联值需遵循Codable,编译器自动合成编码解码逻辑,编码后格式为字典(包含case键和关联值键)。
    • 自定义关联值编码格式:需手动实现encode(to:)init(from:),如将关联值直接编码为单值。

(八)复杂场景处理

  1. 让第三方类型遵循Codable
    • 第三方类型未遵循Codable时,通过扩展手动实现EncodableDecodable,或通过“包装器模式”封装(如struct WrappedThirdParty: Codable { let value: ThirdPartyType; ... })。
  2. 类的Codable实现
    • 类需确保所有存储属性遵循Codable,继承自NSObject的类需避免@objc属性冲突,必要时手动实现编码解码。
  3. 解码多态集合
    • Codable默认不支持多态(如[Animal]包含DogCat实例),需自定义实现:
      • 方案1:用枚举包装多态类型(enum AnimalType: Codable { case dog(Dog), cat(Cat) })。
      • 方案2:编码时添加“类型标识”字段,解码时根据标识动态创建对应实例。

(九)编码和解码的常见任务

  1. JSON与实例互转
    • 编码:JSONEncoder().encode(instance)Data
    • 解码:JSONDecoder().decode(InstanceType.self, from: data) → 实例。
  2. 属性列表(PropertyList)互转
    • 使用PropertyListEncoderPropertyListDecoder,适用于本地配置存储等场景。
  3. 默认值填充
    • 解码时通过decodeIfPresent读取可选值,结合nil合并运算符设置默认值(如name = try container.decodeIfPresent(String.self, forKey: .name) ?? "匿名")。

二、重点知识点总结

(一)Codable协议的核心价值:类型安全的序列化

  • 简化序列化逻辑:编译器自动合成实现,无需手动处理JSON与模型的映射,减少模板代码。
  • 类型安全保障:编码解码过程中通过编译时类型检查,避免“键名拼写错误”“类型不匹配”等运行时问题。
  • 多格式支持:统一适配JSON、PropertyList等多种外部格式,通过不同编码器/解码器切换,接口一致。

(二)自动合成与CodingKeys的灵活运用

  • 自动合成的条件:存储属性全遵循Codable是自动合成的前提,结构体、枚举、类均适用(类需无自定义初始化器冲突)。
  • CodingKeys的核心作用
    • 映射键名:解决Swift属性名与外部数据键名不一致问题。
    • 筛选属性:未包含在CodingKeys中的属性不参与编码解码,适用于临时属性、计算属性。

(三)自定义编码解码的核心场景

  • 数据转换:如日期格式转换、数值单位转换、枚举与原始值映射。
  • 容错处理:如属性缺失时填充默认值、无效数据过滤(如负数价格设为nil)。
  • 复杂结构适配:如嵌套JSON的扁平化、动态键名的处理(如键名包含日期)。

(四)复杂类型的编码解码策略

  • 枚举:带关联值的枚举需注意编码格式,必要时手动实现以适配外部数据结构。
  • 多态集合:通过枚举包装或类型标识字段,解决Codable默认不支持多态的问题。
  • 第三方类型:优先使用扩展手动实现Codable,避免修改第三方源码,保持代码解耦。

三、难点知识点总结

(一)容器操作的逻辑梳理

  • 难点:编码解码时需根据数据结构选择正确的容器类型(键值/无键/单值),容器嵌套时容易混淆层级关系。
  • 常见陷阱
    • 误将无键容器当作键值容器使用(如数组编码时用keyedContainer)。
    • 键值容器的CodingKeys与写入/读取的键不匹配,导致编码解码失败。
  • 解决方案:明确数据结构(如JSON是对象还是数组),按“外部格式→容器类型→属性映射”的逻辑逐步实现。

(二)多态集合的解码实现

  • 难点:Codable解码时需提前确定目标类型,无法直接解码多态集合(如[Animal]包含不同子类实例),手动实现时需处理类型判断与实例创建的逻辑。
  • 示例陷阱:直接解码[Animal].self时,解码器无法区分DogCat,导致解码失败。
  • 解决方案
    • 枚举包装法:用枚举统一多态类型,解码后再拆包(如enum AnimalType: Codable { case dog(Dog), cat(Cat) })。
    • 类型标识法:编码时添加type字段(如"type": "dog"),解码时先读取type,再动态解码为对应类型。

(三)自定义编码解码的边界处理

  • 难点:手动实现时需覆盖所有属性的编码解码,处理可选值、默认值、无效数据等边界场景,容易遗漏或逻辑错误。
  • 常见问题
    • 遗漏部分属性的编码/解码,导致数据丢失。
    • 未处理nil值或无效数据(如字符串转整数失败),导致解码崩溃。
  • 解决方案
    • 编码时按CodingKeys逐一确认属性,避免遗漏。
    • 解码时优先使用decodeIfPresent处理可选值,结合do-catch捕获类型转换错误,添加容错逻辑。

(四)与Objective-C类型的兼容

  • 难点:继承自NSObject的类遵循Codable时,可能因@objc属性、动态派发等特性导致编码解码异常。
  • 常见冲突
    • NSObjectdescription等属性与CodingKeys冲突。
    • 动态属性(dynamic)的编码解码行为与Swift原生属性不一致。
  • 解决方案
    • 显式定义CodingKeys,排除不需要编码的NSObject继承属性。
    • 避免在Codable类中过度使用@objcdynamic,必要时手动实现编码解码逻辑。

(五)嵌套复杂结构的适配

  • 难点:外部数据(如JSON)嵌套层级较深时,编码解码需逐层处理容器,逻辑繁琐且容易出错。
  • 示例场景:JSON包含多层嵌套对象(如{ "user": { "profile": { "name": "张三" } } })。
  • 解决方案
    • 按嵌套层级定义对应的模型结构体,使模型结构与外部数据结构一致。
    • 编码解码时按“外层容器→内层容器→属性”的顺序逐层操作,避免跨层级访问容器。

四、总结

本章核心围绕Codable协议展开,核心逻辑是“通过协议抽象统一编码解码接口,编译器自动合成简化常规场景,手动实现适配复杂需求”。重点在于掌握Codable的自动合成条件、CodingKeys的键映射、容器的正确使用,以及自定义编码解码的核心场景;难点集中在多态集合的处理、容器操作的逻辑梳理、边界场景的容错,以及与复杂类型(如第三方类型、Objective-C类)的兼容。

实际开发中,应遵循“优先自动合成,必要时手动实现”的原则:

  • 简单模型(属性名与外部键名一致、无复杂逻辑)依赖编译器自动合成,提升开发效率。
  • 复杂场景(键名映射、数据转换、容错处理)通过CodingKeys和手动实现encode(to:)/init(from:)解决。
  • 多态、嵌套结构等难点场景,采用枚举包装、分层建模等策略,平衡代码简洁性与兼容性。

通过掌握这些知识点,可实现类型安全、高效简洁的序列化逻辑,避免传统手动解析JSON的繁琐与风险。

如果需要,我可以帮你整理编码解码核心API对比表,或针对某个难点(如多态集合解码、动态键名处理)提供详细代码示例。当前文件内容过长,豆包只阅读了前 14%。