类型推理是Swift类型系统的一个关键特性,在该语言的语法中起着重要作用-通过消除对手动类型注释(编译器本身可以推断出各种值的类型)的需要,从而减少冗长。

类型推断的美妙之处在于很容易忘记它就在那里,尽管对于我们编写的几乎每一行代码来说它都是繁重的工作。然而,通过在设计api时考虑类型推理 - 特别是那些使用泛型类型的 - 我们可以进一步减少代码中的冗长和“噪音”。

本周,让我们看看如何利用类型推理的强大功能,使Swift的CodableAPI变得更漂亮,使用起来更简洁。

Decoding

如果你以前没有使用过codeable,它是Swift的内置API,用于编码和解码与二进制数据之间的可序列化值。它主要用于JSON编码,并包含编译器级别的集成,以自动合成编写序列化代码时通常需要的大量样板文件。

Codable 实际上只是一个简单的类型别名,用于组合 Decodable 协议和 Encodable 协议,这两种协议分别定义了可以解码或编码的类型的需求。

首先,让我们看看在从JSON数据解码 Decodable 值时如何使用类型推断。这里,我们使用默认的方法将一组数据解码为User实例,然后将其传递给一个函数,让登录系统知道用户已经登录:

let decoder = JSONDecoder()
let user = try decoder.decode(User.self, from: data)
userDidLogin(user)

上面的代码实际上并没有什么问题,但是让我们看看是否可以通过使用类型推理使它变得更好。首先,让我们在Data上添加一个扩展,让我们直接解码任何可解码的类型,只需调用decoded():

extension Data {
    func decoded<T: Decodable>() throws -> T {
        return try JSONDecoder().decode(T.self, from: self)
    }
}

因为我们没有使用参数来指定我们要解码的类型,所以我们把它留给Swift的类型系统来使用它的推理功能来解决它。我们所要做的就是告诉它某个值应该被当作什么类型处理,然后编译器会找出剩下的部分。这让我们在解码数据时使用一个非常好的语法,只需使用as关键字来指定类型,就像这样:

let user = try data.decoded() as User

更好的是,由于我们现在使用的是类型推断,当从上下文中已经知道类型时,我们根本不需要指定类型。这意味着我们现在可以将之前的三行解码代码减少为一行,直接将任何解码后的值传递给userDidLogin函数 -并且因为它接受一个用户,这就是类型系统在解码时会使用的:

try userDidLogin(data.decoded())

Pretty sweet! 🍭

Improving flexibility

我们新的类型推理支持的可解码API非常好用,使用起来也很方便,但是它还需要一些额外的灵活性。首先,当前在解码时总是内联创建一个新的JSONDecoder实例,这使得它不可能重用解码器或使用特定设置设置一个解码器。 这是我们可以用完全向后兼容的方式很容易解决的问题,通过使用默认参数来参数化解码器:

extension Data {
    func decoded<T: Decodable>(using decoder: JSONDecoder = .init()) throws -> T {
        return try decoder.decode(T.self, from: self)
    }
}

像我们上面所做的那样,参数化依赖关系不仅增加了灵活性,还提高了代码的可测试性——因为我们基本上已经开始使用依赖注入了。要了解更多关于这种依赖注入和其他依赖注入类型的信息,请查看“Swift中不同类型的依赖注入”。

现在可以在我们的新API中使用任何JSONDecoder,但它仍然强烈地依赖于JSON作为源格式。虽然这对大多数应用程序来说可能没问题,但一些代码库也使用Property Lists进行序列化——这一点Codable也完全支持开箱即用。 如果我们能让新的基于类型推理的API支持任何一种可编码兼容的序列化机制,不是很好吗?

好消息是,这可以通过使用协议轻松实现。不幸的是,没有针对所有解码器类型的通用内置协议,但我们可以很容易地创建自己的解码器。我们所要做的就是将decode函数提取到协议中,然后通过扩展使所有我们感兴趣的解码器符合它,像这样:

protocol AnyDecoder {
    func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T
}

extension JSONDecoder: AnyDecoder {}
extension PropertyListDecoder: AnyDecoder {}

最后,我们可以更新我们的数据扩展来使用我们的新协议,为了方便起见,仍然使用新的JSONDecoder实例作为默认值:

extension Data {
    func decoded<T: Decodable>(using decoder: AnyDecoder = JSONDecoder()) throws -> T {
        return try decoder.decode(T.self, from: self)
    }
}

方便和灵活性-最好的两个世界!😀

Encoding

现在,让我们看看如何做同样的事情,就像我们对编码所做的解码一样。 我们将从创建一个类似于解码的协议开始,并使JSONEncoder和PropertyListEncoder符合它:

protocol AnyEncoder {
    func encode<T: Encodable>(_ value: T) throws -> Data
}

extension JSONEncoder: AnyEncoder {}
extension PropertyListEncoder: AnyEncoder {}

接下来,让我们扩展Encodable以使用我们的新协议,为了最大限度地方便,同样使用默认值:

extension Encodable {
    func encoded(using encoder: AnyEncoder = JSONEncoder()) throws -> Data {
        return try encoder.encode(self)
    }
}

我们现在也可以非常容易地对任何可编码的值进行编码,只需对其调用encoded(),如下所示:

let data = try user.encoded()

对于编码,我们实际上并不经常使用类型推断,因为我们编码的类型总是数据-但与使用可编码API的默认方式相比,我们仍然减少了冗长。

Decoding containers

最后,让我们看看在使用解码容器时如何利用类型推断。与所有第三方JSON框架相比,Codable的主要优势在于它与编译器紧密集成,如果JSON结构与我们在代码中构造值的方式相匹配,它还可以自动合成一致性。

然而,情况并非总是如此,有时我们需要编写可解码和可编码的手动一致性,以便告诉系统如何在JSON和我们的类型之间进行转换。让我们说我们的应用程序处理视频,并有一个视频结构来表示视频,像这样:

struct Video {
    let url: URL
    let containsAds: Bool
    var comments: [Comment]
}

必须编写手动可解码实现的一个常见原因是希望使用默认值。默认情况下,如果JSON数据中缺少一个值,它将被视为一个错误——这并不总是我们想要的。所以为了让视频支持默认值,我们可能会以这样的实现结束:

extension Video: Decodable {
    enum CodingKey: String, Swift.CodingKey {
        case url
        case containsAds = "contains_ads"
        case comments
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKey.self)
        url = try container.decode(URL.self, forKey: .url)
        containsAds = try container.decodeIfPresent(Bool.self, forKey: .containsAds) ?? false
        comments = try container.decodeIfPresent([Comment].self, forKey: .comments) ?? []
    }
}

上面有很多关于类型的冗长和手动说明,所以让我们尝试使用类型推断来改进它。

首先,我们将使用两个方法扩展KeyedDecodingContainerProtocol——一个方法允许我们指定默认值,另一个方法在给定的键没有找到值的情况下失败-两者使用相同的策略,作为我们的解码扩展从以前-依赖于编译器来推断类型,而不是要求我们在调用点指定类型:

extension KeyedDecodingContainerProtocol {
    func decode<T: Decodable>(forKey key: Key) throws -> T {
        return try decode(T.self, forKey: key)
    }

    func decode<T: Decodable>(
        forKey key: Key,
        default defaultExpression: @autoclosure () -> T
    ) throws -> T {
        return try decodeIfPresent(T.self, forKey: key) ?? defaultExpression()
    }
}

上面我们使用@autoclosure来避免不必要地计算默认表达式,以防它不需要。关于在Swift中使用自动闭包的更多信息,请参阅“在设计Swift api时使用@autoclosure”。

现在我们可以更新之前的可解码实现,大大减少了冗长和更干净的调用站点,因为编译器可以在我们直接将结果值赋给属性时推断出所有类型:

extension Video: Decodable {
    enum CodingKey: String, Swift.CodingKey {
        case url
        case containsAds = "contains_ads"
        case comments
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKey.self)
        url = try container.decode(forKey: .url)
        containsAds = try container.decode(forKey: .containsAds, default: false)
        comments = try container.decode(forKey: .comments, default: [])
    }
}

更容易阅读!👍

Conclusion

类型推断是一种非常强大的工具,不仅在调用api时如此,在设计api时也是如此。 通过依赖编译器来解析正确的类型,而不是要求API用户手工指定它们,我们最终可以得到更清晰的代码,更易于阅读,API也更易于使用。

然而,过度依赖类型推断也存在一些危险。首先,它可能是构建时间较慢的常见原因,因为编译器必须做更多的工作才能对表达式进行类型检查(特别是在涉及操作符或多个重载的情况下)。 其次,通过删除显式类型注释,我们可能会使一些代码变得更难理解。与往常一样,始终要考虑何时以及如何设计带有类型推断的api,并且不要做得太过,这一点很重要。

原文链接