利用扩展来约束泛型
原文:Specialized extensions using generic type constraints
将Swift强大的泛型系统与任何Swift类型都可以与新的API和功能扩展这一事实相结合,使我们能够编写有针对性的扩展,在类型或协议符合特定要求时有条件地向其添加新功能。
这一切都从where
关键字开始,它允许我们在一系列不同情况下应用泛型类型约束。在本文中,让我们看看该关键字如何应用于扩展,以及可以通过这样做解锁哪种模式。
基于泛型类型限制扩展
我们可以使用更具体的API扩展泛型类型或协议的方法之一是对扩展本身应用基于where
的类型约束。例如,在这里,我们正在扩展标准库的Sequence
协议(Array
和Set
等集合符合该协议),这是一个方便的API,该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()
}
}
要了解有关上述inout
关键字的更多信息,以及它如何使我们能够在某些上下文中传递对值的引用,请查看“在Swift中使用值语义”。
上述模式的好处是,它使我们添加的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
的元素也符合Equatable
和Codable
等协议时,使其符合这些协议),在我们自己的代码中也可以非常有用——特别是在构建自定义库时。
自我约束
类型约束扩展还可以让我们添加默认协议实现,这些实现只能由满足特定要求的类型使用。例如,当AUIViewController子类遵守Dismissable
协议的dismiss
方法时,我们在这里提供了该协议的默认实现:
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开发人员技能中的绝佳工具。
希望您认为这篇文章有用,如果您有任何问题、评论或反馈,请随时通过电子邮件联系。
感谢您的阅读!