前言
本文翻译自# What is the “some” keyword in Swift?,文章是原作者 19 年介绍 some 关键字的,近期补充了 Swift 5.7 中对 some 的一些扩展,翻译质量有待提高,欢迎交流。
正文
如果你已经在 SwiftUI 上花费过一些时间或者已经看过今年关于 SwiftUI 的 WWDC 视频的话,你可能已经注意到了 SwiftUI 的 View 协议持有一个 some View 类型的 body属性。some 关键字是 Swift 5.1 新引入的,他是不透明属性(SE-0244)功能的一部分。那么 some 关键字什么是?又应该如何使用它呢?
我这篇文章的目的就是回答这些问题。我们首先介绍什么是不透明返回类型(opaque result type),更具体的介绍他们解决了哪一类问题。接下来,我们看看如何在 SwiftUI 中使用不透明类型,最终来说说它是否会成为你在某些编程场景下选择使用的 Swift 功能。
介绍不透明类型(opaque result types)
为了完全理解不透明类型解决的问题,最好对泛型有一个扎实的认识。如果你还不熟悉泛型,我推荐你阅读这些我写的博客文章:
- An introduction to generics in Swift using its built-in types
- Building flexible components with generics and protocols
如果你对学习泛型不感兴趣,只是想学习不透明返回类型以及了解 some 关键字的作用,那也可以。但是要注意,如果不了解泛型,你可能会对这篇文章中的一些上下文内容感到困惑。
在 Swift 中,我们可以使用协议给我们的对象定义接口或者约束。当遵循一个协议时,我们知道它可以实现一些功能,或者持有一些属性。这意味着你可以像下面示例一样来编写代码:
protocol ListItemDisplayable {
var name: String { get }
}
struct Shoe: ListItemDisplayable {
let name: String
}
var listItem: ListItemDisplayable = Shoe(name: "a shoe")
当使用 listItem 对象时,只有通过 ListItemDisplayable 协议暴露出来的属性才可访问。这在你想持有一个 ListItemDisplayable元素类型的列表时会非常有用,可以使数组元素类型不仅限于 Shop 类型:
struct Shoe: ListItemDisplayable {
let name: String
}
struct Shorts: ListItemDisplayable {
let name: String
}
var mixedList: [ListItemDisplayable] = [Shoe(name: "a shoe"),
Shorts(name: "a pair of shorts")]
编译器将 Shoe 和 Shorts 对象视为 ListItemDisplayable 类型,我们的列表不知道正在处理的是 shoes,shorts jeans 或者其他任何东西。他只知道无论元素是什么类型,都会遵循 ListItemDisplayable 协议。
使用关联类型的协议中的不透明返回类型
上节内容中所展示出的灵活性已经是很酷的功能了,但我们仍然可以进一优化代码:
protocol ListDataSource {
associatedtype ListItem: ListItemDisplayable
var items: [ListItem] { get }
var numberOfItems: Int { get }
func itemAt(_ index: Int) -> ListItem
}
上文定义的 ListDataSource 包含了一个项目列表 items, 元素类型遵循 ListItemDisplayable 协议。我们可以使用实现了协议要求的对象作为列表视图的数据源,这看起来很简洁。
我们可以定义一个视图模型类的生成器对象(ViewModelGenerator),依赖传入的项目类型生成一个 ListDataSource 对象:
struct ShoesDataSource: ListDataSource {
let items: [Shoe]
var numberOfItems: Int { items.count }
func itemAt(_ index: Int) -> Shoe {
return items[index]
}
}
struct ViewModelGenerator {
func listProvider(for items: [Shoe]) -> ListDataSource {
return ShoesDataSource(items: items)
}
}
然而,这样编写的代码是无法通过编译,因为 ListDataSource 是存在关联类型约束的协议。我们可以通过将 ShoesDataSource 作为返回类型替代 ListDataSource 来解决此问题。但这样会暴露出我们想对ViewModelGenerator 的使用方隐藏的具体实现细节。 listprovider(for:) 的调用者实际上只需要知道我们将在方法中返回一个 ListDataSource就可以,可以重写generator 生成器代码以通过编译:
struct ViewModelGenerator {
func listProvider(for items: [Shoe]) -> some ListDataSource {
return ShoesDataSource(items: items)
}
}
通过使用 some 关键字,编辑器可以强制执行一些约束条件,同时对 listprovider(for:) 的调用者隐藏该细节:
- 返回值遵循
ListDataSource - 返回对象的关联类型符合
listDataSource设置的所有要求 - 我们通过
listProvider(for:)返回的总是相同的类型
有趣的是最后一项,在 Swift 中,我们依赖于编译器在编译期间进行的大量类型检查来帮助我们编写安全一致性的代码,反过来,编译器也使用这些类型信息优化我们的代码以使其能尽可能快的运行。协议因为暗含动态性会导致编译器在编译期间的一些优化行为变得尤为困难,需要在运行时做类型检查的工作去保证逻辑有效性,这也意味着我们会有一些微不足道的性能损耗。
因为 Swift 编译器可以执行以上几个约束条件,所以它可以像优化具体类型一样来优化 some的使用。并且我们通过不透明返回类型具备了对函数或属性调用者隐藏具体类型的能力。
不透明类型和自我要求
因为编译器在编译期的强类型约束,我们可以做一些有趣的事情。例如,我们可以比较通过不透明类型返回的对象,但是用协议返回的对象是无法比较的。看下示例:
protocol ListItemDisplayable: Equatable {
var name: String { get }
}
func createAnItem() -> ListItemDisplayable {
return Shoe(name: "a comparable shoe: (UUID().uuidString)")
}
上文内容因为 Equatable 协议中包含 Self而无法通过编译,它想比较两个相同类型的不同实例对象 。这导致我们不能使用 ListItemDisplayable 作为返回类型,因为协议自身没有类型信息,我们需要 some 关键字让编译器在调用 createAnItem()时可以确定 ListItemDisplayable 的类型
func createAnItem() -> some ListItemDisplayable {
return Shoe(name: "a comparable shoe: (UUID().uuidString)")
}
编译器现在可以确定,这个函数总是返回 Shoe 类型,所以它可以进一步关联 createAnItem() 返回值中的 Self 具体的类型信息,返回值是遵循 Equatable协议的,允许我们创建两个对象来对比他们是否相等:
let left = createAnItem()
let right = createAnItem()
print(left == right)
这里的 left 和 right 隐藏了他们的类型信息,当你调用 createAnItem()时,你只需知道你可以获取了一个列表项,并且可以和同一个函数返回的其他列表项做对比。
作为反向泛型的不透明类型
Swift 文档有时也会将不透明类型称为反向泛型,这是一个很好的描述。在不透明类型功能之前,唯一一种使用关联类型协议作为返回类型的方式是作为泛型的约束条件使用。缺点是函数调用者可以决定函数返回类型,而不是函数本身决定:
protocol ListDataSource {
associatedtype ListItem: ListItemDisplayable
var items: [ListItem] { get }ƒ
var numberOfItems: Int { get }
func itemAt(_ index: Int) -> ListItem
init(items: [ListItem])
}
func createViewModel<T: ListDataSource>(for list: [T.ListItem]) -> T {
return T.init(items: list)
}
func createOpaqueViewModel<T: ListItemDisplayable>(for list: [T]) -> some ListDataSource {
return GenericViewModel<T>(items: list)
}
let shoes: GenericViewModel<Shoe> = createViewModel(for: shoeList) // 反向泛型需要调用者显示指定泛型类型为 Shoe
let opaqueShoes = createOpaqueViewModel(for: shoeList) // 不透明类型是函数指定类型为 Shoe
上面代码都返回了相同的具体类型 GenericViewModel。不同点在于第一个 case 调用者显式指定了它想通过列表数据生成 GenericViewModel<Shoe>类型模型类,而 some 示例中,调用者仅仅知道它获取了一个包含 ListItemDisplayable 列表的 some ListDataSource 对象。 createOpaqueViewModel 函数实现可以指定它想生成什么对象。在这个示例中,我们返回了一个通用的泛型视图模型类。也可以返回其他不同类型的视图模型类。重要的点是,从函数中返回的类型总是相同的,并且返回的对象遵循 ListDataSource协议 。
在你的项目中使用不透明类型
当我学习不透明类型并且尝试写一些 demo 时,我发现想要在日常项目中使用不透明返回类型不是很容易的事情。在 SwiftUI 中,它发挥着重要作用,这使你相信一些场景下,不少项目都会使用不透明类型。
但是就我个人而言,我不认为如此。不透明类型是针对一些领域内的具体问题的解决方案,但是多数人并不从事相关领域的工作。如果你正在从事构建跨多个项目和代码仓库的框架,或者是编写高度可重用的代码工作,那么不透明类型是非常适合你的,作为框架的构建者,你可能希望基于关联类型的协议编写灵活的代码,返回完全可控的具体类型,并且不向调用者暴露任何泛型类型。
不透明类型的另一个考虑因素则是它在运行时的性能问题,如前文所述,协议有时会强制编译器将类型检查推迟到运行时,这会带来性能损耗,而不透明类型可以帮助编译器进行编译期优化,这非常酷,但我认为这对于大多数应用来说都不是很重要,除非你编写的代码必须针对核心进行优化,我认为运行时的性能损耗不足以支撑不透明类型在代码库的中使用,除非它对你真的很有意义,或者您确信性能优势是值得的。
我真正想表述的是,作为返回类型使用的协议不会对性能造成严重的影响,实际上, 它有时是实现灵活性代码的唯一方式,例如,当你的函数需要返回超过一种具体类型时,而类型取决于一些参数,使用不透明类型则无法实现该逻辑。
这让我想到了在你的代码中使用不透明类型最无趣但是最简单的方法,如果你的代码里有指定协议作为返回类型的场景,但是你知道,你只会从函数中返回一种具体的类型,这时使用不透明类型是有意义的,实际上,Swift 团队正在考虑在 Swift 6.0 中将协议作为类型的一种,这可能永远不会出现在 Swift 6.0 中,但是这表明,在你任何想去尝试使用的时候,Swift 团队都认为 `some 是默认的一种实现思路。
更有意思的考虑因素是,无论你怎么定义简单的泛型,都可以使用 some 来代替。例如在下面的示例中,你可以使用 some 替代泛型:
class MusicPlayer {
func play<Playlist: Collection<Track>>(_ playlist: Playlist) { /* ... */ }
}
在这个例子中,我们的 play方法有一个泛型参数 Playlist, 它遵循 Collection 协议约束,该协议包含 Track 类型对象。我们可以使用 Swift 5.7 中的主要关联类型来编写这个约束代码,可以在What are primary associated types in Swift 5.7? 中学习更多关于 primary associated types 的内容,如果我们只是在一个地方使用 Playlist泛型类型,例如函数参数的位置。从 Swift 5.7 开始我们可以使用 some 代替泛型,Swift 5.7 允许我们使用 some 修饰函数参数是一个巨大的进步。
重写代码示例如下:
class MusicPlayer {
func play(_ playlist: some Collection<Track>) { /* ... */ }
}
这样更好了不是吗?
总结
在本文中你了解了不透明类型解决的问题,并通过几个示例向您展示了如何使用它,你需要记住当你想返回一个遵循关联类型的协议约束的时候。不透明类型可以充当返回值类型,因为编译器在编译期可以推导出协议关联类型的具体类型,所以这是可行的。你还看到出于相似的原因,不透明类型可以解决所谓的 Self约束要求。接下来,你还看到某些场景下不透明类型如何作为反向泛型来使用的。这可以让函数实现来决定返回的遵循协议的具体类型而不是调用者决定。
其次,针对你的 apps 中可能使用到不透明类型的地方给出了一些意见,随着 Swift 5.7 允许在更多的地方使用 some 修饰,而不再仅限于返回值类型,我认为 some 将变得非常有用。这将帮助我们使用具体类型而不是 existentials type(协议),这使我们的代码更高效和健壮。
如果你有任何问题、反馈或者使用不透明类型的最佳应用,只要是我在本文中没提到的,都欢迎在 Twitter 上进行交流。
\