4

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

 2 years ago
source link: https://kemchenj.github.io/2018-07-09/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK