欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Swift4.0 Codable踩坑之派生类数据的保存

程序员文章站 2022-05-31 17:17:07
...

本以为之前使用Codable的过程中踩的坑已经够多了,今天博主有遇到一个坑,调了一个下午才解决,问题不大,但是中文的技术文里都很少涉及这个问题。

问题描述:遵循了Codable协议的自定义类,派生出的子类JSON化与反JSON化。

简单来说,就是这儿有一个类遵循了Codable协议,其自身可以很方便地使用JSONEncoder和JSONDecoder来JSON化和反JSON化。但是子类就不是那么方便了。系统提示需要实现一个require init?(from:)的初始化方法。通过这次踩坑,博主总结出了两点在使用Codable的过程中需要注意的点。

首先我们通过一个例子来说明吧。
实现一个Person类,包含属性firstName(姓)、lastName(名)、fullName(全名)、age(年龄)和gender(性别)。gender需要使用枚举类型。包含初始化方法,并且可以使用print直接输出,并可以JSON和反JSON化。
再实现一个Student类,继承自Person,并包含自己的stuNo(学号)和department(公寓)属性。department属性需要使用枚举类型。包含初始化方法,并且可以使用print直接输出,并可以JSON和反JSON化。

看要求两个类需要实现的功能不多且相似,本以为会很简单地解决,但编程就是这么有趣。
首先来实现Person类,我们需要一个性别的枚举

//性别的枚举
enum Gender: Int {
    case male    //男性
    case female  //女性
    case unknow  //未知
}

然后就可以实现Person类了。

//人类
class Person: CustomStringConvertible, Codable {
    var firstName: String  //姓
    var lastName: String  //名
    var age: Int  //年龄
    var gender: Gender  //性别

    var fullName: String {  //全名
        get {
            return firstName + lastName
        }
    }

    //构造方法
    init(firstName: String, lastName: String, age: Int, gender: Gender) {
        self.firstName = firstName
        self.lastName = lastName
        self.age = age
        self.gender = gender
    }

    //实现CustomStringConvertible协议中的计算属性,可以使用print直接输出对象内容
    var description: String {
        return "fullName: \(self.fullName), age: \(self.age), gender: \(self.gender)"
    }
}

Person类的实现非常方便,其中的fullName博主选择使用计算属性,因为它与姓和名相关。然后为了可以使用print输出,Person类遵循了CustomStringConvertible协议,读者也可以继承自NSObject去重写其中的description计算属性,看个人爱好。最后为了可以转换为JSON属性,遵循了Codable协议。编译一下代码,系统报错,说Person类没有遵循Codable协议,然后博主就纳闷儿了。尝试了半天,发现如果自定义的类中嵌套了自定义的类、结构体或枚举,使用Codable协议时,这些嵌套的类型也需要遵循Codable协议,所以,解决方法很简单,直接在Gender枚举上遵循Codable协议就好。
更改代码如下:

enum Gender: Int, Codable {
    case male    //男性
    case female  //女性
    case unknow  //未知
}

然后就是Student类了,基础的实现代码如下:

//学生类
class Student: Person {
    var stuNo: Int  //学号
    var department: Department  //公寓

    //构造方法
    init(stuNo: Int, firstName: String, lastName: String, age: Int, gender: Gender, department: Department) {
        self.stuNo = stuNo
        self.department = department
        super.init(firstName: firstName, lastName: lastName, age: age, gender: gender)
    }

    //重写父类的计算属性
    override var description: String {
        return "stuNo: \(self.stuNo), fullName: \(self.fullName), age: \(self.age), gender: \(self.gender), department: \(self.department)"
    }
}

这时我们就会发现xcode提示你需要实现一个初始化方法,我们点击fix之后自动生成代码如下:

required init(from decoder: Decoder) throws {
        fatalError("init(from:) has not been implemented")
    }

然后,年轻的博主以为问题解决了,试试将Student的实例转换为JSON数据。

//学生实例
var student = Student(stuNo: 20151101001, firstName: "李", lastName: "四", age: 19, gender: .male, department: .two)

print(student)
print()

//JSON化
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let studentData = try encoder.encode(student)

print(String(data: studentData, encoding: .utf8)!)
print()

输出结果如下:
Swift4.0 Codable踩坑之派生类数据的保存
哎?没问题呀,别慌。我们再试试将JSON数据转换为Student实例

//反JSON化
let newStudent = try JSONDecoder().decode(Student.self, from: studentData)
print(newStudent)

很不幸,程序crash了。报错的部分截图如下:
Swift4.0 Codable踩坑之派生类数据的保存

看到红色部分的错误提示,感觉似曾相识,往上看一下代码,WC,这不跟系统自己生成的初始化方法里面的那句话,一毛一样么?
既然这里错了,我们就需要在这里改正。

到这里,博主开始CSDN、简书、知乎各种翻,没有具体的解决代码,于是只有自己试一试了。于是便在改初始化方法中敲下了decoder并用”.”来查看改参数里面有的属性或方法,万幸,不多,一个一个试,发现只有一个container(keyedBy: CodingKey.Protocol)方法可以使用。然后经历几个小时的折磨(一把辛酸泪)。终于找到了解决方法。
我们需要自定义一个遵循CodingKey协议的枚举,可以自定义其枚举值,不过博主推荐跟相应要保存的属性同名,之所以需要定义这个枚举,是decoder的container(容器)需要通过key来反JSON化对应的属性值,具体的实现代码如下:

//自定义CodingKey
enum Key: String, CodingKey {
    case stuNo
    case department
}

使用这个枚举来反序列化相应的属性

required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Key.self)
        self.stuNo = try container.decode(Int.self, forKey: .stuNo)
        self.department = try container.decode(Department.self, forKey: .department)

        try super.init(from: decoder)
    }

到这里,博主的第六感告诉自己,应该还没完(其实吧,是运行继续崩溃)。因为我们只使用自定义的CodingKey枚举来反JSON化,而并没有使用它来JSON化。所以,我们需要重写一个方法

override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: Key.self)
        try container.encode(stuNo, forKey: Key.stuNo)
        try container.encode(department, forKey: Key.department)

        try super.encode(to: encoder)
    }

到这里,构建、运行程序,结果如下:
Swift4.0 Codable踩坑之派生类数据的保存
其实这两个方法,在你遵循的那个类中,在编译之后,系统会将其生成在代码当中,我们只是将Person类中的这两个方法重写了而已。

所有的代码如下:

//性别的枚举
enum Gender: Int, Codable {
    case male    //男性
    case female  //女性
    case unknow  //未知
}

//公寓的枚举
enum Department: String, Codable {
    case one, two, three
}


//人类
class Person: CustomStringConvertible, Codable {
    var firstName: String  //姓
    var lastName: String  //名
    var age: Int  //年龄
    var gender: Gender  //性别

    var fullName: String {  //全名
        get {
            return firstName + lastName
        }
    }

    //构造方法
    init(firstName: String, lastName: String, age: Int, gender: Gender) {
        self.firstName = firstName
        self.lastName = lastName
        self.age = age
        self.gender = gender
    }

    //实现CustomStringConvertible协议中的计算属性,可以使用print直接输出对象内容
    var description: String {
        return "fullName: \(self.fullName), age: \(self.age), gender: \(self.gender)"
    }
}

//自定义CodingKey
enum Key: String, CodingKey {
    case stuNo
    case department
}

//学生类
class Student: Person {
    var stuNo: Int  //学号
    var department: Department  //公寓

    //构造方法
    init(stuNo: Int, firstName: String, lastName: String, age: Int, gender: Gender, department: Department) {
        self.stuNo = stuNo
        self.department = department
        super.init(firstName: firstName, lastName: lastName, age: age, gender: gender)
    }

    //编码方法
    override func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: Key.self)
        try container.encode(stuNo, forKey: Key.stuNo)
        try container.encode(department, forKey: Key.department)

        try super.encode(to: encoder)
    }

    //解码方法
    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: Key.self)
        self.stuNo = try container.decode(Int.self, forKey: .stuNo)
        self.department = try container.decode(Department.self, forKey: .department)

        try super.init(from: decoder)
    }

    //重写父类的计算属性
    override var description: String {
        return "stuNo: \(self.stuNo), fullName: \(self.fullName), age: \(self.age), gender: \(self.gender), department: \(self.department)"
    }
}

//学生实例
var student = Student(stuNo: 20151101001, firstName: "李", lastName: "四", age: 19, gender: .male, department: .two)

print(student)
print()

//JSON化
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let studentData = try encoder.encode(student)

print(String(data: studentData, encoding: .utf8)!)
print()

//反JSON化
let newStudent = try JSONDecoder().decode(Student.self, from: studentData)
print(newStudent)

最后博主总结如下:
1、遵循了Codable协议的类,其嵌套类型也需要遵循Codable协议;
2、遵循了Codable协议的类的派生类,需要重写override func encode(to encoder: Encoder) throws方法和required init(from decoder: Decoder) throws方法。这两个方法都需要使用容器来编码和解码,容器中属性的key与自定义遵循了CodingKey协议的枚举值对应,在将子类中的属性编码和解码之后,需要调用父类的编码或解码方法。