在Swift 5.7中使用“some”和“any”关键字来引用范型协议
将Swift灵活的泛型系统与面向协议的编程相结合,通常可以导致一些真正强大的实现,同时最大限度地减少代码重复,并能够在代码库中建立明确定义的抽象水平。然而,在Swift 5.7之前编写此类代码时,遇到以下编译器错误是很常见的:
Protocol ’X‘ can only be used as a generic constraint because it
has Self or associated type requirements.
--
协议“X”只能用作范型约束,因为它
有Self或关联类型要求
让我们看看Swift 5.7(目前作为Xcode 14的一部分处于测试阶段)如何引入一些关键的新功能,旨在使上述错误成为过去。
不透明参数类型
就像我们在问与答的文章“为什么某些协议,如Equatable
和Hashable
,不能直接引用? ”中仔细研究一样,使用范型协议时经常遇到上述编译器错误的原因是,一旦协议定义了关联类型,编译器就会开始限制该协议的引用方式。
例如,假设正在开发一个处理各种组的应用程序,为了能够尽可能多地重用组处理代码,我们选择将核心的Group
类型定义为范型协议,让每个实现类型定义它包含的Item
值:
protocol Group {
associatedtype Item
var items: [Item] { get }
var users: [User] { get }
}
现在,由于该关联类型Item
,我们无法直接引用 Group
协议——即使在与组的Items
无关的代码中也是如此,例如计算从给定组的用户列表中显示哪些名称的函数:
// Error: Protocol ‘Group’ can only be used as a generic constraint
// because it has Self or associated type requirements.
func namesOfUsers(addedTo group: Group) -> [String] {
group.users.compactMap { user in
isUserAnonymous(user) ? nil : user.name
}
}
当使用低于5.7的Swift版本时,解决上述问题的一种方法是让namesOfUsers
函数范型化,然后按上述错误消息提示,仅使用Group
协议作为范型约束——像这样:
func namesOfUsers<T: Group>(addedTo group: T) -> [String] {
group.users.compactMap { user in
isUserAnonymous(user) ? nil : user.name
}
}
当然,这种技术没有问题,但与使用非范型协议或任何其他形式的Swift类型(包括具体的泛型类型)相比,它确实使函数声明变得更加复杂。
谢天谢地,Swift 5.7扩展了some
关键字(在Swift 5.1中引入),使其也适用于函数参数,干净地解决了的此类问题。因此,就像声明SwiftUI视图从其body
属性返回 some View
一样,现在也可以让 namesOfUsers
函数接受some Group
作为其输入:
func namesOfUsers(addedTo group: some Group) -> [String] {
group.users.compactMap { user in
isUserAnonymous(user) ? nil : user.name
}
}
就像使用some
关键字来定义不透明的返回类型一样(就像在构建SwiftUI视图时所做的那样),编译器将自动推断在每个调用点传递给函数的实际具体类型,而无需编写任何额外的代码。干脆利落!
主要关联类型
然而,有时,我们可能想对给定的参数添加一些要求,而不仅仅是要求它符合某个协议。例如,假设现在正在开发一个应用程序,允许用户将他们最喜欢的文章添加为书签,如下, bookmarkAritcles
方法允许将文章数组传递到书签:
class BookmarksController {
...
func bookmarkArticles(_ articles: [Article]) {
...
}
}
然而,并非所有的调用点都可能使用数组存储文章。例如,以下ArticleSelectionController
使用字典来跟踪在UITableView
或UICollectionView
中为哪个IndexPath
选择的文章。因此,当将该文章集合传递到bookmarkArticles
方法时,首先需要手动将其转换为数组——像这样:
class ArticleSelectionController {
var selection = [IndexPath: Article]()
private let bookmarksController: BookmarksController
...
func bookmarkSelection() {
bookmarksController.bookmarkArticles(Array(selection.values))
...
}
}
如果 bookmarkArticles
方法能够适用于任何包含文章值的集合不是更好吗?简单地将其参数类型更改为some Collection
行不通,因为这不足以指定正在寻找的具有特定Element
类型作为输入的集合。
然而,可以再次使用一组范型类型约束来解决这个问题:
class BookmarksController {
...
func bookmarkArticles<T: Collection>(
_ articles: T
) where T.Element == Article {
...
}
}
同样,这没有什么问题——但Swift 5.7再次引入了一种更轻量级的方式来表达上述声明,其工作方式与专门化具体的泛型类型(如Array<Article>
)时完全相同。也就是说,现在可以通过在协议名称后的角括号中添加该类型,简单地告诉编译器我们的输入集合应包含什么Element
类型:
class BookmarksController {
...
func bookmarkArticles(_ articles: some Collection<Article>) {
...
}
}
非常酷!我们甚至可以嵌套这些类型的声明——因此,如果我们想让我们的BookmarksController
能够为符合范型ContentItem
协议的任何类型的值添加书签,那么可以将some ContentItem
指定为集合的预期Element
类型,而不是使用具体的Article
类型:
protocol ContentItem: Identifiable where ID == UUID {
var title: String { get }
var imageURL: URL { get }
}
class BookmarksController {
...
func bookmark(_ items: some Collection<some ContentItem>) {
...
}
}
上述工作要归功Swift 新特性主关联类型,以及Swift的Collection
协议将Element
声明为此类关联类型,例如:
protocol Collection<Element>: Sequence {
associatedtype Element
...
}
当然,作为适当的Swift特性,我们也可以在自己的协议中使用主要关联类型(语法完全相同)。
存在的和“any”关键字
最后,让我们更进一步,将ArticleSelectionController
转换为范型类型,可用于选择任何符合ContentItem
的值,而不仅仅是文章。由于我们现在希望混合多个都符合同一协议的具体类型,some
关键字不起作用——因为正如之前看到的,它的工作原理是让编译器为每个调用点推断单个具体类型,而不是多个类型。
这就是新的any
关键字(在Swift 5.6中引入)的来源,它使我们能够将ContentItem
协议称为存在的
。现在,这样做确实有一定的性能和内存影响,因为它实际上是自动形式的类型擦除,但在动态存储异构元素集合的情况下,它非常有用。
例如,通过简单地使用any ContentItem
作为selection
字典的值类型,现在可以在该字典中存储符合该协议的任何值:
class ContentSelectionController {
var selection = [IndexPath: any ContentItem]()
private let bookmarksController: BookmarksController
...
func bookmarkSelection() {
bookmarksController.bookmark(selection.values)
...
}
}
然而,上述更改确实引入了一个新的编译器错误,因为BookmarksController
期望收到一个包含所有具有完全相同类型的值的集合——而在新的ContentSelectionControlle
实现中并非如此。
谢天谢地,解决这个问题很简单,只需将一些some ContentItem
替换为bookmark
方法声明中的any ContentItem
:
class BookmarksController {
...
func bookmark(_ items: some Collection<any ContentItem>) {
...
}
}
甚至可以混合any
和some
引用,编译器将自动在两者之间进行翻译。例如,如果针对单元素重载bookmark
方法,使其简单地调用第一个bookmark
方法,可以这样做(即使第一个方法的items
包含any ContentItem
,第二个方法也接受some ContentItem
):
class BookmarksController {
...
func bookmark(_ items: some Collection<any ContentItem>) {
for item in items {
bookmark(item)
}
}
func bookmark(_ item: some ContentItem) {
...
}
}
同样,重要的是要强调,使用any
确实在幕后引入类型擦除,即使这一切都是由编译器自动完成的——因此使用静态类型(使用some
关键字时仍然如此)绝对是尽可能首选的方式。
结论
Swift 5.7不仅使Swift的泛型系统更强大,而且可以说也使其更易于访问,因为它减少了使用范型类型约束和其他更高级的范型编程技术以能够引用某些协议的需要。
泛型绝对不是解决每个问题的正确工具,但事实证明,能够以更轻量级的方式使用Swift泛型系统绝对是一个巨大的胜利。
感谢您的阅读!