Swift 5 7 中的主要关联类型是什么

28 阅读4分钟

原文:What are primary associated types in Swift 5.7? – Donny Wals

Swift 5.7 引入了许多涉及泛型和协议的新功能。在这篇文章中,我们将探讨一个极其强大的新功能,即所谓的 "主要关联类型"。在本篇文章结束时,你将知道并理解什么是主要关联类型,以及为什么我认为它们对于帮助你写出更好的代码是极其重要和强大的。

如果你熟悉 Swift 5.6 或更早的版本,你可能知道带有关联类型的协议一直是个有趣的野兽。它们有时很难使用,在 Swift 5.1 之前,每当我们想使用带有关联类型的协议时,我们总是不得不求助于泛型的使用。请看下面这个例子:

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

这个例子在 Swift 5.1 中无法编译,今天在 Swift 5.7 中仍然无法编译。原因是 Collection 有各种关联类型,如果我们想使用 Collection,编译器必须能够填入这些类型。例如,我们需要知道我们的集合持有什么类型的元素。

在我们的代码中使用带有关联类型的协议,一个常见的变通方法是使用一个被限制在协议中的泛型:

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

如果你不太清楚这个例子的作用,可以看看我写的这篇帖子,了解更多关于使用泛型和关联类型的信息。

我们没有把 Collection 作为一个存在类型(一个容纳遵守 Collection 协议的对象的盒子),而是把 Collection 作为一个泛型的约束,我们称之为 Playlist。这意味着编译器将始终知道哪个对象是用来填充 Playlist 的。

在 Swift 5.1 中,引入了 some 关键字,结合 Swift 5.7 在函数参数上使用 some 关键字的功能,我们可以写出以下内容:

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

要了解更多关于 some 关键字的信息,我建议你看一下这篇文章,它解释了你需要知道的关于 some 的一切。

这很好,但是泛型解决方案和 some 解决方案都有一个重要问题。我们不知道 Collection 里面是什么。可能是 String,可能是 Track,可能是 Album,没有办法知道。这使得 func play(_ playlist: some Collection) 对我们的 MusicPlayer 来说实际上是无用的。

**在 Swift 5.7 中,协议可以指定主要关联类型。**这些关联类型很像泛型。它允许开发者为一个给定的关联类型指定类型,作为一个泛型约束

对于 Collection,Swift 库为 Element 关联类型添加了一个主要关联类型。

这意味着,当你把元素传递给一个函数时,你可以指定它必须在一个 Collection 中,比如我们的 func play(_ playlist: some Collection) 。在我向你展示之前,让我们先看看协议是如何定义主要关联类型的:

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
}

注意该协议有多个关联类型,但在 Collection 协议上只有 Element 被写在 <> 之间。这是因为 Element 是一个主要关联类型。在处理一个集合时,我们通常不关心它是哪种迭代器。我们只想知道集合里面有什么!

因此,为了使我们的播放列表专业化,我们可以写下面的代码:

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

注意,如果 Playlist 只用在一个地方,上面的功能等同于下面的内容:

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

虽然上面的两个片段在功能上是等同的,但前者使用 some 的选项是首选。原因是使用 some 的代码比使用一个不需要成为泛型的泛型更容易阅读和推理。

请注意,这也适用于 any 关键字。例如,如果我们想在 MusicPlayer 上存储我们的播放列表,我们可以写下面的代码:

class MusicPlayer {
    var playlist: **any** Collection<Track> = []

    func play(_ playlist: some Collection<Track>) {
        self.playlist = playlist
    }
}

有了主要关联类型,我们可以写出更有表现力、更强大的代码,我很高兴看到 Swift 语言的这一补充。