【译】What are primary associated types in Swift 5.7?

238 阅读3分钟

前言

本文译自 What are primary associated types in Swift 5.7?, 介绍 Swift 5.7 中新引入的 primary associated types 应用场景。这个新的特性改动不大,但是某些场景下非常有用,建议学习掌握。

正文

Swift 5.7 引入了一些新特性,涉及泛型和协议系统。本文将探索一个及其强大的特性功能,被称为“primary associated types” 。读完本文后,你将了解什么是 primary associated types,以及为什么我说它是非常强大并且重要的新特性。

如果你熟悉 Swift 5.6 或者更早的版本,你可能了解具有关联类型的协议是很有趣的。他们有时很难用,在 Swift 5.1之前每当要使用关联类型的协议都不得不求助于泛型实现。考虑下面的示例:

 class MusicPlayer {
   func play(_ playlist: Collection) { /* ... */ } 
 }

示例在 Swift 5.1中无法编译通过。在现在的 Swift 5.7 中也一样。原因在于 Collection包含各种关联类型,编译器必须能够推导出具体类型。例如我们需要什么类型的 Element在我们的集合中。

使用关联类型协议的通用解决方法是使用泛型来遵循该协议:

 class MusicPlayer<Playlist: Collection> {
   func play(_ playlist: Playlist) { /* ... */ } 
 }

如果你不确定这个例子是用来做什么,可以看下这篇文章using generics and associated types.。

我们将 Collection 协议作为泛型 Playlist 的约束来代替 Collection作为 existential type (看做包含遵守 Collection 元素类型的盒子)使用。所以编译器总是知道 Playlist是什么类型的对象。

在 Swift 5.1 中, some 关键字被引入。结合 Swfit 5.7 中扩展 some 修饰函数参数的能力,我们可以编写以下代码:

 class MusicPlayer {
   func play(_ playlist: some Collection) { /* ... */ } 
 }

了解更多关于 some的内容,推荐阅读What is the “some” keyword in Swift?

无论是泛型亦或是 some 解决方案,都有一个重要的问题。我们不知道集合中包含的是什么类型。可以是 String、可以是Track也可以是Album。这导致我们 MusicPlayer 中的 func play(_ playlist: some Collection) 几乎没有任何作用。

在 Swift 5.7 中,协议可以指定 primary associated types,关联类型有一点像泛型,二者都允许开发者指定给定的关联类型作为泛型的约束。

例如 Collection,Swift 标准库给关联类型 Element 新添加了primary associated types 。

意味着当你传递参数给 func play(_playlist: some Collection)时,可以指定集合 Collection 中的元素类型,在此之前,我们看下协议如何定义 primary associated type:

 public protocol Collection<Element> : Sequence {
 ​
   associatedtype Element
   associatedtype Iterator = IndexingIterator<Self>
   associatedtype SubSequence : Collection = Slice<Self> where Self.Element == Self.SubSequence.Element, Self.SubSequence == Self.SubSequence.SubSequence
 ​
   // a lot of other stuff
 }

注意协议有多个关联类型,但是只有 Element<>包含。因为 Element 是一个 primary associated type。当使用集合时,我们经常不关心生成什么样的 iterator 迭代器。我们只知道集合 Collection 包含元素类型即可!

可以这样优化 playlist:

 class MusicPlayer {
   func play(_ playlist: some Collection<Track>) { /* ... */ }
 }

当只有一个地方使用Playlist时,上面的代码等价于以下代码:

 class MusicPlayer {
   func play<Playlist: Collection<Track>>(_ playlist: Playlist) { /* ... */ }
 }

译者补充: 注意这里强调的仅有一个地方使用时,some才可以和泛型这样等价转换

虽然两个代码片段是等价的,但是 some仍然是首选。原因在于相比于泛型, some 因为没有引入泛型类型更易读。

这里 any 也是可以使用的,例如如果我们的 MusicPlayer想存储保留 playlist,我们也可以这样编写:

 class MusicPlayer {
     var playlist: any Collection<Track> = []
 ​
     func play(_ playlist: some Collection<Track>) {
         self.playlist = playlist
     }
 }

使用 primary associated type 我们可以写出更强大并且更具表现力的代码,我很高兴 Swift 新增了这一特性。