或许你并不需要重写 init(from:) 方法

Codable 作为 Swift 的特性之一也是很注重安全,也很严谨,但它对于“严谨”和“安全”的定义不一定跟别的语言一样,这就导致了它在实际使用时总会有这样那样的磕磕绊绊,我们不得不重写 init 方法去让它跟外部环境融洽地共存。最近在工作中这样的事情发生多了,我也就不得不想办法去解决它。

严格的类型解析

最开始遇到了第一个问题就是 Bool 的解析,我们后端的接口习惯使用 01 整数去表达布尔值,解析失败之后,我第一感觉是这会不会是个 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,不应该混到一起用。

还有一个比较棘手的问题,URLinit?(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 内部开始查找,那就尝试一下:

Screen Shot 2018-07-09 at 20.36.15

成功了,那么就回到我们最初的目的,把 URLBool 也重载掉:

Screen Shot 2018-07-09 at 20.41.40

并且这种重载的方法是用的是直接派发,所以我们可以控制这个函数的作用范围:

// 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:) 函数啦!🎉🎉🎉