在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
*/
以上便是博主个人的一些看法,如有错误,还请各位看客纠正。
本文转自:https://blog.csdn.net/average17/article/details/78236589