Swift自定义类的存储

在Swift开发中,许多时候会涉及到存储自定义的类,不管是存储到本地文件还是远程服务器,都会涉及到编码和解码的问题。下面就来介绍一下在Swift中怎么存储自定义的类。
在Swift中存储自定义的类有两种方法,一种是Swift 3.0版本的NSCoding,还有一种是Swift 4.0版本的Codable。我们就两种方法来谈谈其中的区别吧。

Swift 3.0: NSCoding

在NSCoding这个协议在OC中就存在,所以自然而然的,Swift也能使用它。使用NSCoding协议,需要这个类继承自NSObject并遵循NSCoding协议,而且还要实现NSCoding协议中的两个方法,才能编码和解码自定义的类。例子如下:

class Person: NSObject, NSCoding {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    //编码
    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: "name")
        aCoder.encode(age, forKey: "age")
    }

    //解码
    required init?(coder aDecoder: NSCoder) {
        self.name = aDecoder.decodeObject(forKey: "name") as! String
        self.age = aDecoder.decodeInteger(forKey: "age")
    }
}

其中编码和解码两个方法就是NSCoding协议中要求实现的两个方法,只有实现了这两个方法,才能将自定义的类序列化和反序列化。接下来,我们就要创建一个Person类的实例,并保存到本地文件中,和从本地文件中读取内容。

let person = Person(name: "Tom", age: 18)

//拿到一个本地文件的URL
let manager = FileManager.default
var url = manager.urls(for: .documentDirectory, in: .userDomainMask).first
url?.appendPathComponent("test.txt")

//序列化Person实例
let dataWrite = NSKeyedArchiver.archivedData(withRootObject: person)

do {
    //将数据写入文件
    try dataWrite.write(to: url!)
} catch {
    print("保存到本地文件失败")
}

//从本地文件读取数据
if let dataRead = try? Data(contentsOf: url!) {
    //反序列化数据,并将结果强转为Person类的实例
    let person = NSKeyedUnarchiver.unarchiveObject(with: dataRead) as! Person
    //输出内容
    print("name: \(person.name)")  //输出: name: Tom
    print("age: \(person.age)")  //输出: age: 18
} else {
    print("文件不存在,读取本地文件失败")
}

可以看出,当第一步完成之后,保存自定义类就变得非常简单了,只需要使用系统定义好的NSKeyedArchiver类和NSKeyedUnarchiver类就可以了。

Swift 4.0: Codable

接下来再看看Swift 4.0的Codable协议怎么保存自定义类。
在Swift 4.0版本,Apple推出了一个新的协议叫Codable。只要类或者结构体遵循了这个协议,就能够很方便地将类或者结构体的实例转换为JSON数据,并且不需要在类或者结构体中实现编码和解码方法,可以看出,Codable协议比NSCoding协议更加方便、简洁。废话不多说,来看看具体的实现:

class Person: Codable {
    var name: String
    var age: Int

    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }
}

let person = Person(name: "Tom", age: 18)

//拿到一个本地文件的URL
let manager = FileManager.default
var url = manager.urls(for: .documentDirectory, in: .userDomainMask).first
url?.appendPathComponent("test.txt")

//将Person实例转换为JSON数据
let dataWrite = try JSONEncoder().encode(person)
do {
    try dataWrite.write(to: url!)
} catch {
    print("保存到本地文件失败")
}

//从本地文件读取数据
if let dataRead = try? Data(contentsOf: url!) {
    //将数据转换为Person实例
    let person = try JSONDecoder().decode(Person.self, from: dataRead)
    //输出内容
    print("name: \(person.name)")  //输出: name: Tom
    print("age: \(person.age)")  //输出: age: 18
} else {
    print("文件不存在,读取本地文件失败")
}

可以看出,Codable协议比NSCoding协议方便很多,而且代码看起来也简洁了很多。

Codable的坑

那可能就有人想问了,既然有了Codable协议,而且还这么简洁,那么NSCoding协议的存在还有什么意义呢?这个问题博主当初也想了很久,后来在一次偶然的机会,发现了Codable协议的不足。先卖个关子,我们来看一个需求:
要求:现有一个AnimalProtocol协议,该协议有一个eat()方法,然后实现两个类Cat和Dog,这两个类都有一个类型为String的name属性,并且需要遵循AnimalProtocol协议,实现eat()方法。创建一个数组,数组中保存若干个Cat实例和Dog实例,将此数组保存到本地文件中,并从文件中读取到相应的内容。

先来看看NSCoding协议是怎么实现这个需求的:

//定义协议
protocol AnimalProtocol {
    func eat()
}

//定义Cat类
class Cat: NSObject, NSCoding, AnimalProtocol {
    var name: String

    init(name: String) {
        self.name = name
    }

    func eat() {
        print("cat \(self.name) eat")
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: "name")
    }

    required init?(coder aDecoder: NSCoder) {
        self.name = aDecoder.decodeObject(forKey: "name") as! String
    }
}

//定义Dog类
class Dog: NSObject, NSCoding, AnimalProtocol {
    var name: String

    init(name: String) {
        self.name = name
    }

    func eat() {
        print("dog \(self.name) eat")
    }

    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: "name")
    }

    required init?(coder aDecoder: NSCoder) {
        self.name = aDecoder.decodeObject(forKey: "name") as! String
    }
}

//创建一个空数组(因为这个数组要同时保存两个类的实例,所以需要以它们共同的一个类型来创建数组)
var animalArray = [AnimalProtocol]()

//然后用循环创建多个Cat和Dog实例,并加入数组中
for i in 1...3 {
    let cat = Cat(name: "cat\(i)")
    let dog = Dog(name: "dog\(i)")

    animalArray.append(cat)
    animalArray.append(dog)
}

let manager = FileManager.default
var url = manager.urls(for: .documentDirectory, in: .userDomainMask).first
url?.appendPathComponent("test.txt")

let dataWrite = NSKeyedArchiver.archivedData(withRootObject: animalArray)
do {
    try dataWrite.write(to: url!)
} catch {
    print("保存到本地文件失败")
}

if let dataRead = try? Data(contentsOf: url!) {
    let newArray = NSKeyedUnarchiver.unarchiveObject(with: dataRead) as! [AnimalProtocol]

    for item in newArray {
        item.eat()
    }
}
//输出如下
/*
cat cat1 eat
dog dog1 eat
cat cat2 eat
dog dog2 eat
cat cat3 eat
dog dog3 eat
*/

可以看到,虽然步骤有一点点麻烦,但是还是能很方便地实现我们的需求的,接下来,我们再看看Codable协议是怎么做的吧。
先上代码:

//定义协议
protocol AnimalProtocol: Codable {
    func eat()
}

//定义Cat类
class Cat: AnimalProtocol {
    var name: String

    init(name: String) {
        self.name = name
    }

    func eat() {
        print("cat \(self.name) eat")
    }
}

//定义Dog类
class Dog: AnimalProtocol {
    var name: String

    init(name: String) {
        self.name = name
    }

    func eat() {
        print("dog \(self.name) eat")
    }
}

var animalArray = [AnimalProtocol]()

for i in 1...3 {
    let cat = Cat(name: "cat\(i)")
    let dog = Dog(name: "dog\(i)")

    animalArray.append(cat)
    animalArray.append(dog)
}

let manager = FileManager.default
var url = manager.urls(for: .documentDirectory, in: .userDomainMask).first
url?.appendPathComponent("test.txt")

let dataWrite = try JSONEncoder().encode(animalArray)

do {
    try dataWrite.write(to: url!)
} catch {
    print("保存到本地文件失败")
}

if let dataRead = try? Data(contentsOf: url!) {
    let newArray = try JSONDecoder().decode([AnimalProtocol].self, from: dataRead)

    for item in newArray {
        item.eat()
    }
} else {
    print("文件不存在,读取本地文件失败")
}

看代码没错,而且能通过编译,但是一运行,直接crash。还报了一个莫名其妙的错误,错误是这样写的:fatal error: Array does not conform to Encodable because AnimalProtocol does not conform to Encodable.: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-900.0.65/src/swift/stdlib/public/core/Codable.swift, line 3962
2017-10-14 19:43:37.565213+0800 study[5383:411423] fatal error: Array does not conform to Encodable because AnimalProtocol does not conform to Encodable.: file /Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-900.0.65/src/swift/stdlib/public/core/Codable.swift, line 3962
我去!这就郁闷了,明明我的类遵循了Codable协议,你却说没有遵循,这是什么鬼,而且就算用扩展让Array遵循协议也不可以

extension Array: Codable {
}

难道是遇到鬼了?不会吧!
后来博主请教导师,才知道原来这是Codable的一个不足点。遵循了Codable协议的类的数组,其中的元素必须为同一个类型。虽然这里是用协议类型去创建的数组,但其元素却是遵循了该协议类型的类的实例。所以本质上不尽相同。

如果在这里将AnimalProtocol协议改成一个类的话,虽然在运行时不会崩溃,但却无法将其向下转换为其子类了。代码如下:

//定义父类
class Animal: Codable {
    func eat() {
        print("Animal")
    }

}

//定义Cat类
class Cat: Animal {
    var name: String

    init(name: String) {
        self.name = name
        super.init()
    }

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

    override func eat() {
        print("cat \(self.name) eat")
    }
}

//定义Dog类
class Dog: Animal {
    var name: String

    init(name: String) {
        self.name = name
        super.init()
    }

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

    override func eat() {
        print("dog \(self.name) eat")
    }
}

var animalArray = [Animal]()

for i in 1...3 {
    let cat = Cat(name: "cat\(i)")
    let dog = Dog(name: "dog\(i)")

    animalArray.append(cat)
    animalArray.append(dog)
}

let manager = FileManager.default
var url = manager.urls(for: .documentDirectory, in: .userDomainMask).first
url?.appendPathComponent("test.txt")

let dataWrite = try JSONEncoder().encode(animalArray)

do {
    try dataWrite.write(to: url!)
} catch {
    print("保存到本地文件失败")
}

if let dataRead = try? Data(contentsOf: url!) {
    let newArray = try JSONDecoder().decode([Animal].self, from: dataRead)

    for item in newArray {
        if item is Cat {
            (item as! Cat).eat()
        } else if item is Dog {
            (item as! Dog).eat()
        } else {
            item.eat()
        }
    }
} else {
    print("文件不存在,读取本地文件失败")
}
/*
输出结果为:
Animal
Animal
Animal
Animal
Animal
Animal
*/

以上便是博主个人的一些看法,如有错误,还请各位看客纠正。

发布了45 篇原创文章 · 获赞 20 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/average17/article/details/78236589
今日推荐