将Swift强大的泛型系统与任何Swift类型都可以用新的api和功能进行扩展的事实相结合,使我们能够编写有针对性的扩展,当类型或协议符合特定要求时,有条件地向其添加新特性。

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

Constraining an extension based on a generic type

我们可以用更具体的api扩展泛型类型或协议的方法之一是对扩展本身应用基于where的类型约束。例如,这里我们扩展标准库的Sequence协议(数组和集合设置符合)和一个方便的API,让我们直接在一个序列上通过调用render()呈现一系列Renderable-conforming类型,只要这个序列包含此类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()
    }
}

上述模式的好处是,它使我们添加的渲染方法的内部实现完全类型安全(因为我们有一个编译时保证,我们将始终使用Renderable元素),现在,当我们想要渲染符合Renderable值的数组时,我们可以访问一个整洁方便的API:

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

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

另一种编写受类型约束的扩展可能非常有用的情况是,当我们希望有条件地使泛型类型符合协议时。例如,只有当Swift的标准数组类型包含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的标准库中被广泛使用(例如,当数组的元素也符合Equatable和Codable协议时,就使数组符合这些协议),并且在我们自己的代码中也非常有用——特别是在构建自定义库时。

Self constraints

类型约束扩展还可以使我们添加只能由满足特定需求的类型使用的默认协议实现。例如,这里我们提供了一个默认实现的Dismissable协议的dismiss方法,当该协议被UIViewController的子类遵循时:

protocol Dismissable {
    func dismiss()
}

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

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

class ProductViewController: UIViewController, Dismissable {
    ...
}

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

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

Applying constraints to individual functions

最后,让我们看看如何不仅将泛型类型约束应用于整个扩展,还应用于此类扩展中的单个函数。例如,如果我们想,我们可以像这样写之前的序列扩展:

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

在上述情况下,我们是将泛型类型约束应用于扩展还是直接应用于函数实际上并不重要——这两种方法都给我们带来了完全相同的效果。然而,情况并非总是如此。为了进一步研究,让我们假设我们正在开发一个包含以下Group协议的应用程序,该协议使用泛型关联类型来允许每个组定义它包含的成员值的类型:

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值是不够的—groups本身需要匹配,这可能不是我们想要的。

因此,为了解决这个问题,让我们修改上面的扩展,使用直接附加到组合方法的泛型类型约束,这使得两个组可以是不同的类型,同时仍然要求它们的成员类型相同:

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进行更高级的泛型编程时,它非常有用。

Conclusion

Swift版本的泛型和扩展本身都非常有用,但当它们结合在一起时,我们可以采用一些更有趣、更强大的模式。当然,并不是每个代码库都需要利用这些功能——不要过度概括我们编写的代码非常重要——但是,如果有必要,类型约束扩展可以成为我们Swift开发人员工具箱中的一个很好的工具。