Swift:`where` 关键字的应用扩展介绍及教程

185 阅读5分钟

结合Swift强大的泛型系统和任何Swift类型都可以用新的API和功能进行扩展这一事实,我们可以编写有针对性的扩展,在符合某些要求时有条件地给一个类型或协议增加新的功能。

这一切都始于where 关键字,它让我们在一系列不同的情况下应用通用类型约束。在这篇文章中,让我们看看这个关键字如何应用于扩展,以及这样做可以解锁什么样的模式。

基于通用类型的扩展的限制

我们可以用更具体的API来扩展一个通用类型或协议,其中一个方法是对扩展本身应用一个基于where 的类型约束。例如,在这里我们用一个方便的API来扩展标准库的Sequence 协议(像ArraySet 这样的集合符合该协议),该API让我们通过在一个序列上直接调用render() 来呈现一系列符合Renderable 的类型--只要该序列包含这样的Renderable 元素。

protocol Renderable {
    func render(using renderer: inout Renderer)
}

extension Sequence where Element == Renderable {
    func render() -> UIImage {
        var renderer = Renderer()

        for member in self {
            member.render(using: &renderer)
        }

        return renderer.makeImage()
    }
}

上述模式的好处是,它使我们添加的render 方法的内部实现完全是类型安全的(因为我们有一个编译时的保证,即我们总是与Renderable 元素打交道),而且我们现在只要想渲染一个符合Renderable 的值的数组,就可以获得一个整洁的便利 API:

class CanvasViewController: UIViewController {
    var renderables = [Renderable]()
    private lazy var imageView = UIImageView()
    ...

    func render() {
        imageView.image = renderables.render()
        ...
    }
}

另一种情况是,当我们想有条件地使一个通用类型符合一个协议时,编写一个类型约束的扩展会非常有用。例如,我们可以让 Swift 的标准Array 类型只在它包含Renderable 元素时才符合上述Renderable 协议:

extension Array: Renderable where Element: Renderable {
    func render(using renderer: inout Renderer) {
        for member in self {
            member.render(using: &renderer)
        }
    }
}

有了上述做法,我们现在就可以使用Renderable 值的嵌套数组(这在分组方面可能有很大的好处),同时仍然能够像以前一样渲染我们的顶层renderables 数组:

extension CanvasViewController {
    func userDidDrawShapes(_ shapes: [Shape]) {
        renderables.append(shapes)
        render()
    }
}

上述模式在 Swift 的标准库中被广泛使用(例如,当Array 的元素也符合EquatableCodable 等协议时,使其符合这些协议),并且在我们自己的代码中也非常有用--特别是在构建自定义库时。

自我约束

类型约束的扩展也可以使我们添加默认的协议实现,这些协议只能被满足某些要求的类型使用。例如,这里我们提供了一个Dismissable 协议的dismiss 方法的默认实现,当该协议被一个UIViewController 子类所符合时:

protocol Dismissable {
    func dismiss()
}

extension Dismissable where Self: UIViewController {
    func dismiss() {
        dismiss(animated: true)
    }
}

上述模式的好处是,它让我们选择我们的视图控制器成为Dismissable ,而不是在我们整个应用程序中的所有UIViewController 实例中添加该方法。同时,由于我们的扩展,我们不需要在每一个视图控制器中重新实现dismiss 方法,而是可以符合我们的新协议并利用其默认实现。

class ProductViewController: UIViewController, Dismissable {
    ...
}

我们仍然可以选择在需要时提供一个自定义的dismiss 实现,对于不是UIViewController 子类的类型,提供这样一个专门的实现是必须的(因为这些类型不能访问我们约束扩展的默认实现)。

class AlertPresenter: Dismissable {
    func dismiss() {
        ...
    }
}

将约束应用于单个函数

最后,让我们来看看我们如何不仅将通用类型约束应用于整个扩展,而且也应用于这种扩展中的各个函数。例如,如果我们想的话,我们可以把我们之前的Sequence 扩展写成这样:

extension Sequence /*where Element == Renderable*/ {
    func render() -> UIImage where Element == Renderable {
        ...
    }
}

在上述情况下,我们是将通用类型约束应用于我们的扩展,还是直接应用于我们的函数,其实并不重要--两种方法给我们带来的效果完全相同。然而,情况并不总是这样的。为了进一步探讨,假设我们正在开发一个包含以下Group 协议的应用程序,该协议使用一个通用的associatedtype ,使每个组能够定义它所包含的Member 值的类型。

protocol Group {
    associatedtype Member
    var members: [Member] { get }
    init(members: [Member])
}

然后,假设我们想创建一个简单的API,通过合并两个组的members 数组来进行组合--这可以不使用任何形式的通用约束,例如像这样:

extension Group {
    func combined(with other: Self) -> Self {
        Self(members: members + other.members)
    }
}

然而,上述扩展确实要求被合并的两个组的类型完全相同。也就是说,它们仅仅包含相同类型的Member 值是不够的--组本身需要匹配,这可能不是我们想要的结果。

所以,为了解决这个问题,让我们修改上面的扩展,使用一个直接连接到我们的combined 方法的通用类型约束,这使得两个组可以有不同的类型,同时仍然要求它们的Member 类型是相同的:

extension Group {
    func combined<T: Group>(
        with other: T
    ) -> Self where T.Member == Member {
        Self(members: members + other.members)
    }
}

有了上面的规定,我们现在可以很容易地将组与组之间进行任意组合,只要这些组存储的是同一类的值。例如,这里我们将一个ArticleGroup 实例与一个FavoritesGroup 结合起来,这是可能的,因为它们都存储了Article 的值。

let articles: ArticleGroup = ...
let favorites: FavoritesGroup = ...
let combined = articles.combined(with: favorites)

很好!虽然上述模式肯定比我们在本文中看到的其他模式更具体,但在Swift中进行更高级的通用编程时,它可能会非常有用。

总结

Swift版本的泛型和扩展本身就非常有用,但当它们结合在一起时,可以让我们采用一些更加有趣和强大的模式。当然,并不是每一个代码库都需要利用这些功能--不要把我们写的代码过度泛化,这一点非常重要--但是,如果有必要,类型受限的扩展可以成为我们Swift开发者工具箱中的一个伟大工具。

谢谢你的阅读!