Swift 协议
hudson 译 原文
许多语言都支持协议的概念(有时也称为“接口”),但是 Swift 将协议视为其整体设计的真正基石 —— 甚至苹果公司都称 Swift 为“协议导向编程语言”。
基本上,协议使我们能够定义 API 和要求,而不将它们与特定类型或实现绑定。例如,假设我们正在开发某种形式的音乐播放器,并且我们当前已将播放代码实现为 Player 类中的两个单独方法 —— 一个用于播放歌曲,另一个用于播放专辑:
class Player {
private let avPlayer = AVPlayer()
func play(_ song: Song) {
let item = AVPlayerItem(url: song.audioURL)
avPlayer.replaceCurrentItem(with: item)
avPlayer.play()
}
func play(_ album: Album) {
let item = AVPlayerItem(url: album.audioURL)
avPlayer.replaceCurrentItem(with: item)
avPlayer.play()
}
}
从上面的实现中可以看出,我们确实有相当多的代码重复,因为我们的两个 play 方法都需要做几乎完全相同的事情 —— 将要播放的资源转换为 AVPlayerItem,然后使用 AVPlayer 实例进行播放。
这是协议可以帮助我们以更加优雅的方式解决的问题之一。首先,定义一个名为 Playable 的新协议,该协议要求符合它的每种类型实现一个 audioURL 属性:
protocol Playable {
var audioURL: URL { get }
}
上面的
get关键字用于指定,一个类型为了符合我们的新协议,只需要声明一个只读的audioURL属性 —— 它不必可写。
然后,我们可以以两种方式使不同类型符合新协议。一种方式是在类型声明本身中声明—— 例如像这样:
struct Song: Playable {
var name: String
var album: Album
var audioURL: URL
var isLiked: Bool
}
另一种方式是通过扩展声明—— 对于已经满足协议所有要求的类型,可以简单地使用空扩展来进行声明(这是我们下面的 Album 模型的情况):
struct Album {
var name: String
var imageURL: URL
var audioURL: URL
var isLiked: Bool
}
extension Album: Playable {}
通过以上更改,我们现在可以大大简化Player 类 —— 通过之前的两个 play 方法合并成一个,而不是接受一个具体类型(如 Song 或 Album),现在接受任何符合新 Playable 协议的类型:
class Player {
private let avPlayer = AVPlayer()
func play(_ resource: Playable) {
let item = AVPlayerItem(url: resource.audioURL)
avPlayer.replaceCurrentItem(with: item)
avPlayer.play()
}
}
这样就好多了!但是,上面协议有一个小问题,那就是它的名称。虽然 Playable 可能最初似乎是一个合适的名称,但它有点表明符合它的类型实际上可以执行播放,这并不是事实。相反,由于我们的协议完全是关于将实例转换为audioURL,所以将其重命名为 AudioURLConvertible —— 以使事情清晰明了:
// 重命名我们的声明:
protocol AudioURLConvertible {
var audioURL: URL { get }
}
// 对于 Song 的符合性:
struct Song: AudioURLConvertible {
...
}
// 专辑的扩展:
extension Album: AudioURLConvertible {}
// 最后我们在 Player 类中使用它的方式:
class Player {
private let avPlayer = AVPlayer()
func play(_ resource: AudioURLConvertible) {
...
}
}
另一方面,现在让我们看看一个确实需要操作(或者换句话说,一个方法)的协议,这使得它非常适合典型的“-able”后缀命名。在这种情况下,我们将要求一个 mutating 方法,因为我们希望使任何符合该协议的类型能够在其实现中改变自己的状态(即更改属性值):
protocol Likeable {
mutating func markAsLiked()
}
extension Song: Likeable {
mutating func markAsLiked() {
isLiked = true
}
}
由于大多数符合我们新 Likeable 协议的类型很可能(无意间的双关语)以与 Song 完全相同的方式实现我们的 markAsLiked 方法要求,所以我们也可以选择将 isLiked 属性作为我们的要求 —— 并且还通过添加 set 关键字要求它是可变的。
protocol Likeable {
var isLiked: Bool { get set }
}
酷的是,如果我们仍希望我们的 API 是 something.markAsLiked(),那么我们可以轻松地通过协议扩展实现这一点 —— 这使我们能够向所有符合给定协议的类型添加新方法和计算属性:
extension Likeable {
mutating func markAsLiked() {
isLiked = true
}
}
要了解更多关于
mutating关键字的信息,以及关于值类型和引用类型的更广泛讨论,请查看这篇基础文章
有了以上内容,我们现在可以使 Song 和 Album 都符合Likeable 而无需编写任何额外的代码 —— 因为它们都已经声明了一个可变的 isLiked 属性:
extension Song: Likeable {}
extension Album: Likeable {}
除了允许代码重用和统一类似实现外,协议在重构时或者我们希望有条件地替换一个实现为另一个实现时也非常有用。
举个例子,假设我们想要测试之前的 Player 类的一个新实现 —— 该实现会对歌曲和其他播放项进行排队,而不是立即开始播放它们。当然,要做到这一点的一种方式是将该逻辑添加到我们的原始 Player 实现中,但这可能很快变得混乱 —— 尤其是如果我们想要执行多个测试并尝试更多种类的变化。
相反,让我们通过为我们的核心播放 API 实现一个协议来创建一个抽象。在这种情况下,我们简单地命名为 PlayerProtocol,并要求其包含我们之前的单个 play 方法:
protocol PlayerProtocol {
func play(_ resource: AudioURLConvertible)
}
使用我们的新协议现在可以自由地实现我们希望的任意多种不同版本的播放器 —— 每个版本可以有自己的私有实现细节,同时仍与完全相同的公共 API 兼容:
class EnqueueingPlayer: PlayerProtocol {
private let avPlayer = AVQueuePlayer()
func play(_ resource: AudioURLConvertible) {
let item = AVPlayerItem(url: resource.audioURL)
avPlayer.insert(item, after: nil)
avPlayer.play()
}
}
extension Player: PlayerProtocol {}
有了以上内容,我们现在可以通过使创建应用程序播放器的任何代码返回一个符合 PlayerProtocol 的实例,而不是一个具体类型,来有条件地使用我们的播放器实现之一:
func makePlayer() -> PlayerProtocol {
if Settings.useEnqueueingPlayer {
return EnqueueingPlayer()
} else {
return Player()
}
}
最后,让我们回到 Swift 被称为“协议导向语言”的最初说法。到目前为止,在本文中,我们确实看到了 Swift 支持许多基于协议的强大特性 —— 但实际上是什么让语言本身成为了协议导向的呢?
在很多方面,这归结于标准库的设计方式 —— 它利用了诸如协议扩展之类的特性来优化自己的内部实现,并通过这些相同的扩展使我们能够在其许多协议之上编写我们自己的功能。
例如, 以标准库标准库的 Collection 协议(所有集合,如 Array 和 Set,都符合该协议)为例,当其存储的元素符合 Numeric 时(Numeric 是另一个标准库协议,数值类型,如 Int 和 Double,都符合该协议),希望给它添加了一个sum方法,
extension Collection where Element: Numeric {
func sum() -> Element {
// reduce 方法是在标准库中使用协议扩展来实现的,
// 这也使得我们能够在我们自己的扩展中使用它:
reduce(0, +)
}
}
要了解为什么我们能够直接将 + 操作符传递给 reduce,请查看关于头等函数的 Swift Clips 节目。
有了以上内容,我们现在可以轻松地对任何数字集合进行求和,例如一个 Int 值数组:
let numbers = [1, 2, 3, 4]
numbers.sum() // 10
所以,协议如此之有用的原因在于它们不仅使我们能够创建抽象,从而隐藏实现细节,并使使用这些接口的代码共享变得更容易 —— 而且它们还使我们能够定制和扩展标准库的各种 API。
协议还有许多方面和特性,这篇基础文章没有涵盖到,比如它们与测试和架构的关系,有关泛型协议的简要介绍,请查看这篇泛型协议基础的文章。 —— 更多协议相关内容,请查看这个列表
感谢阅读!🚀