或许你并不需要重写 init(from:) 方法
Codable 作为 Swift 的特性之一也是很注重安全,也很严谨,但它对于“严谨”和“安全”的定义不一定跟别的语言一样,这就导致了它在实际使用时总会有这样那样的磕磕绊绊,我们不得不重写 init 方法去让它跟外部环境融洽地共存。最近在工作中这样的事情发生多了,我也就不得不想办法去解决它。
严格的类型解析
最开始遇到了第一个问题就是 Bool
的解析,我们后端的接口习惯使用 0
跟 1
整数去表达布尔值,解析失败之后,我第一感觉是这会不会是个 bug,所以去翻了一下 JSONDecoder
的源码:func unbox(_ value: Any, as type: Bool.Type) throws -> Bool? {
...
if let number = value as? NSNumber {
if number === kCFBooleanTrue as NSNumber {
return true
} else if number === kCFBooleanFalse as NSNumber {
return false
}
}
...
}
如果把 ===
改成 ==
就可以很好地解决我的问题,我本来还很天真得以为这真的是个 bug,但在 Twitter 上向开发组的人求证之后,他们表示代码并没有错,就是这么设计的,Boolean 就是 Boolean,Int 就是 Int,不应该混到一起用。
还有一个比较棘手的问题,URL
的 init?(string:)
在传入空字符串的时候会初始化失败,所以在把空字符串解析为 URL
的时候会直接中断整个解析然后抛出错误,还有一个就是数组内部存在 null
元素的时候,如果 Array
的元素不声明为 Optional
的话也是会中断解析。
Swizzle 掉 decode 方法
比起重新自定义一个 Decoder 来说,如果能够 swizzle 掉 decode 方法,直接控制 decode 行为会更加方便。实际上我们真的可以做到,Codable 的原理是自动代码生成,严格来说,它其实不算是编译的一部分:struct Foo: Codable {
var bar: Int?
// <--自动生成的部分
init(from decoder: Decoder) throws {
let container = decoder.container(keyedBy: CodingKeys.self)
bar = container.decodeIfPresent(Int.self, forKey: .bar)
}
// 自动生成的部分-->
}
并且 decodeIfPresent
方法是在 Foundation 框架里的,那么我们能不能在我们的 Module 里也写一个 decodeIfPresent
方法重载掉它呢?因为如果方法是在 extension 里声明并实现的话,方法会优先从 Module 内部开始查找,那就尝试一下:
成功了,那么就回到我们最初的目的,把 URL
和 Bool
也重载掉:
并且这种重载的方法是用的是直接派发,所以我们可以控制这个函数的作用范围:// A 文件
extension KeyedDecodingContainer {
fileprivate func decodeIfPresent(_ type: Int.Type, forKey key: CodingKey) -> Int? { ... }
}
// B 文件
// 这里不会调用到 A 文件里的方法
let b = container.decodeIfPresent(Int.self, forKey: key)
甚至我们可以在 Module 内重载一遍,应对个别特殊情况可以在文件里再重载一遍,达到最佳的灵活度,从某种程度上来说,我认为这甚至是比 Objective-C 的消息机制更加灵活的一种函数声明机制,而且它的影响范围是有限的,不容易对外部模块造成破坏(别声明为 open
或者 public
就没问题)。
我对于 Twitter 上 Swift 开发团队的成员发的一条推印象特别深,他说其实 Swift 也有 Selector 和 IMP 的机制,只不过这个方法选择的过程是在编译时去完成,而并非在运行时去完成的。通过了解方法选择的规则,就可以做到类似于 Swizzle 的效果,这也是 Swift 重载机制有趣而且复杂的地方。
总结
现在大家可以通过这种方法去重构掉项目里那些多余的 init(from:)
函数啦!🎉🎉🎉