背景
有时候我们会遇到需要使用一个数据对象副本的情况. 在OC中因为每一个类都是继承NSObject
, 调用对应copy
方法, 实现copyWithZone
就好.
但是Swift中使用struct
居多, class
没有特殊情况并不会继承NSObjec
. 而且想copy
一个对象就要去实现以下copyWithZone
对每一个属性赋值而且还要考虑属性中是否有引用的问题(大部分情况下我们是需要完全深拷贝的)
OC中想拷贝一个对象需要这样
@interface Person : NSObject
- (nonnull id)copyWithZone:(nullable NSZone *)zone {
Person * person = [[[self class] allocWithZone:zone] init];
person.name = self.name;
person.age = self.age;
person.man = self.man;
return person;
}
注意这样的赋值其实是拷贝对象的引用.
@interface Man: NSObject
@property (assign) int height;
@property (strong) NSString * colleage;
@end
如果我接下来修改copy出来的person.man
的height
或者colleage
, 同样也会修改之前person
对象的man
所以我们一般要在copyWithZone
的方法里赋值时, 再要将引用类型都copy
一下.
这里就不放代码了.这是常识了. 所以OC会用归档解档来实现真正意义上的深拷贝.
这样问题也是很明显的
这个类要conform NSCoping
和NSCoding
的协议.. 而且还要实现NSCoding
要求的归档解档方法. 最终, 调用的这个copy才能达到目的.
如果我还有一个类也要实现copy方法. 可能需要重复上面步骤了. 好像也没有好的办法进行复用.
Swift的类我们需要自己写一个copy方法了.
当然要声明一个协议, 供所有想进行copy
的类调用
protocol Copyable: class {
func copy() -> Self
}
对于结构体struct
每次赋值都是一个新副本, 所以不存在拷贝问题, 我们用class
表示这个协议只能让类来conform.
class Person: Copyable {
var name: String = ""
var age = 3
var man: Man?
func copy() -> Person {
let newPerson = Person()
newPerson.name = self.name
newPerson.age = self.age
newPerson.man = self.man
}
}
这当然不是我们想要达到的目的, 跟上面OC版本面临同样的问题.
解决方案
一开始我想到就是要为copy
提供默认实现, 想要进行copy
的类直接conform协议就具备了copy
功能, 而不是每一个class
都去实现它.
extension Copyable {
/// 使用extension 提供默认实现
func copy() -> Self {
}
}
虽然在协议中通过Self
知道当前类名, 通过反射可以拿到属性与对应的值, 但是不通过KVC还是没办法给一个新实例赋值的..
在这里想到了Swift4出来的codable, 可以把一个对象值encode
编码, 得到的data
再进行decode
解码, 得到的肯定是这个对象的副本. 而且我们不需要知道具体属性值.
所以, 我们需要让Copyable
再conform一下Codable
protocol Copyable: class, Codable {
func copy() -> Self
}
extension Copyable {
func copy() -> Self {
let encoder = JSONEncoder()
guard let data = try? encoder.encode(self) else {
fatalError("encode失败")
}
let decoder = JSONDecoder()
guard let target = try? decoder.decode(Self.self, from: data) else {
fatalError("decode失败")
}
return target
}
}
测试一下
let person = Person()
person.name = "aa"
person.man = Man()
person.man.height = 30
let person2 = person.copy()
person2.name = "bb"
person2.man.height = 40
person.name // "aa"
person.man.height // 30
person2.name // "bb"
person2.man.height // 40
可以看到person的copy完全是一次深拷贝.
而且这个协议可以为任意class添加copy
方法, 无需多余的实现代码.
Codable的强大
也是多亏了Codable, 我们做的只是用一个方法封装了一下.
public typealias Codable = Decodable & Encodable
Encodable
实现了 public func encode(to encoder: Encoder) throws
方法, 把一个模型进行编码.
Decodable
实现了public init(from decoder: Decoder) throws
方法, 把一串data解码为一个目标模型
所以遵循了Codable
的类或结构体可以直接从一段json字符串进行构造.
let jsonString = """
{"name": "aaa",
"age": 3,
"level": 33,
"man":
{"height": 100}
}
"""
let decoder = JSONDecoder()
let jsonData = jsonString.data(using: .utf8)!
let person = try! decoder.decode(Person.self, from: jsonData)
print(person.name, person.man.height) /// "aaa" 100
看起来我们的Copyable
还多了一个从字符串转模型的方法..稍加封装甚至可以作为网络请求完毕后数据转模型的部分呢..
如果json字符串中的键值与属性名称不匹配就需要重写一下协议中规定的方法了.
class Person: Copyable {
var name: String = ""
var age = 3
var level = 30
var man = Man()
enum CodingKeys: CodingKey {
case na
case a
case level
case man
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .na)
self.age = try container.decode(Int.self, forKey: .a)
self.level = try container.decode(Int.self, forKey: .level)
self.man = try container.decode(Man.self, forKey: .man)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.name, forKey: .na)
try container.encode(self.age, forKey: .a)
try container.encode(self.level, forKey: .level)
try container.encode(self.man, forKey: .man)
}
}
Encoder
与Decoder
需要一个遵循了Codingkey的可以提供映射关系的类型. 我们这里直接用枚举的case作为键值即可.
以上代码Github地址
你可能越来越惊讶于这个Codable竟然能做这么多事情.
笔者水平有限. 更感兴趣的朋友可以戳下方了解更多.(如果我有时间学到更多会更新新的一篇来分析一下这个协议具体做的事情)
Swift-JSONEncoder源码