协议(Protocol)是 Swift 的基础功能。在 Swift 的标准库中起着主导作用,并且是一种常见的抽象方法。Protocol 提供了与其他语言类似的接口功能。
这篇文章将介绍面向协议编程(Protocol Oriented Programming,简称 POP),面向协议编程是 Apple 在 WWDC2015 上提出的一种编程范式,其已成为 Swift 的基础。与传统的面向对象编程(Object Oriented Programming,简称 OOP)相比,POP 更为灵活。如果你正在学习 Swift,应掌握面向协议编程。
本文将涉及以下几个方面:
- 面向对象编程与面向协议编程的区别。
- 协议的默认实现。
- 扩展 Swift 标准库。
- 协议支持范型。
1. 介绍
假设你在开发一款赛车游戏,希望玩家能够驾驶汽车、摩托车和飞机,甚至可以骑不同的鸟飞行。这里的关键是可以操作不同的设备。
一种常见的方案是使用面向对象编程,将所有逻辑封装到基类,其他类继承自基类。因此,基类需要有驾驶、飞行等各种逻辑。
开发过程中为每个设备创建一个类。编程过程中,你会发现Car
、Motorcycle
有一些共用功能,你可能需要创建一个共同的父类MotorVehicle
实现共用功能。此外,还会创建一个Aircraft
基类实现飞行相关功能,Plane
继承自Aircraft
。
随着需求的迭代,后续可能需要增加会飞的汽车。Swift 不支持多重继承,应如何同时继承自MotorVehicle
和Aircraft
?是否需要创建另一个基类,实现MotorVehicle
、Aircraft
的功能?当然,也可以通过 Runtime 的消息转发实现多重继承,但其不利于维护,也不优雅。
面向协议编程可以很好解决这一问题。
2. 面向协议编程
协议(protocol)允许将相似的方法、函数、属性放到一组。Swift 中的class
、enum
和struct
都可以遵守协议,但只有class
支持继承。
与继承相比,协议的优势在于对象可以遵守多个协议。
使用面向协议编程,代码可以更具模块化。可以将协议视为功能块,当通过遵守新的协议添加新功能时,无需创建全新的对象。创建全新的对象太耗费时间。相反,只需增加不同的功能块。
将基类模式转变面向协议编程模式,可以很好解决前面遇到的问题。使用协议时,可以创建一个FlyingCar
类,同时遵守MotorVehicle
和Aircraft
协议。
3. 创建协议
创建一个名称为ProtocolOrientedProgramming
的playground,并添加以下代码:
protocol Bird {
var name: String { get }
var canFly: Bool { get }
}
protocol Flyable {
var airspeedVelocity: Double { get }
}
Bird
协议有两个只读的属性。Flyable
协议有一个只读的属性。
在没有使用面向协议编程时,开发者一般创建一个Flyable
的基类,继承后实现子类。使用面向协议编程后,所有的都以 protocol 开始,将所有功能封装到 protocol,无需使用继承。这样在定义类型时可以更为灵活。
4. 遵守协议
添加以下struct
:
struct FlappyBird: Bird, Flyable {
var name: String
let flappyAmplitude: Double
let flappyFrequency: Double
let canFly = true
var airspeedVelocity: Double {
3 * flappyFrequency * flappyAmplitude
}
}
FlappyBird
结构体遵守了Bird
、Flyable
协议。airspeedVelocity
是一个计算属性,FlappyBird
是一种会飞的鸟,canFly
返回true
。
继续添加以下结构体:
struct Penguin: Bird {
let name: String
let canFly = false
}
struct SwiftBird: Bird, Flyable {
var name: String { "Swift \(version)"}
let canFly = true
let version: Double
private var speedFactor = 1000.0
init(version: Double) {
self.version = version
}
var airspeedVelocity: Double {
version * speedFactor
}
}
Penguin
是一种不会飞的鸟。如果使用了继承模式,则会让所有鸟会飞。使用协议可以定义一组功能类似的组件,任何相关的对象都可以遵守该协议。
SwiftBird
结构体有不同版本,版本越高airspeedVelocity
越快。
每个遵守Bird
协议的struct
、class
都需要实现canFly
,即使已经存在了Flyable
协议。如果能为 protocol 提供默认实现,重复代码将变少,这也就是 protocol extension 的用途。
5. Protocol Extension
Protocol extension 提供了协议的默认实现。以下代码为Bird
的canFly
提供了默认实现:
extension Bird {
// Flyable birds can fly.
var canFly: Bool { self is Flyable }
}
遵守Flyable
协议的类型canFly
返回true
,即遵守Bird
协议的类型无需重复实现canFly
属性。现在可以删除FlappyBird
、Penguin
和SwiftBird
中的canFly
属性。
6. enum 也可以遵守协议
Swift 中的enum
比 C、C++ 中的更为强大,它支持了以往只能够用在类、结构体上的功能。例如,enum
可以遵守协议。
添加以下enum
:
// enum也可以遵守协议
enum UnladenSwallow: Bird, Flyable {
case african
case european
case unknown
var name: String {
switch self {
case .african:
return "African"
case .european:
return "European"
case .unknown:
return "What do you mean? African or European?"
}
}
var airspeedVelocity: Double {
switch self {
case .african:
return 10.0
case .european:
return 9.9
case .unknown:
fatalError("You are thrown from the bridge of death!")
}
}
}
UnladenSwallow
遵守了Bird
和Flyable
协议,canFly
使用了 protocol extension 的默认实现。
7. 重写 protocol extension 的默认实现
UnladenSwallow
类型自动使用了Bird
协议canFly
属性的默认实现,使用以下代码可以重写默认实现:
extension UnladenSwallow {
var canFly: Bool {
self != .unknown
}
}
只有在.african
和.european
时canFly
返回true
。使用以下代码进行验证:
UnladenSwallow.unknown.canFly // false
UnladenSwallow.african.canFly // true
Penguin(name: "King Penguin").canFly // false
使用上述方法,可以像面向对象编程一样重写属性、方法。
8. 扩展协议
还可以让自己创建的协议遵守 Swift 标准库中协议,同时定义其默认实现。更新Bird
协议如下:
// Bird协议遵守CustomStringConvertible协议。
protocol Bird: CustomStringConvertible {
var name: String { get }
var canFly: Bool { get }
}
extension CustomStringConvertible where Self: Bird {
var description: String {
canFly ? "I can fly" : "Guess I'll just sit here"
}
}
Bird
协议遵守了CustomStringConvertible
协议,CustomStringConvertible
协议只有一个实例属性description
,实现后可以提供自定义输出。CustomStringConvertible
只为Bird
类型提供了 protocol extension。
添加以下代码:
UnladenSwallow.african
使用Shift + Command + Enter快捷键运行 playground,可以看到 assistant editor 区域输出的I can fly
。
9. 使用 protocol extension 扩展 Swift 标准库
Protocol extension 提供了一种扩展命名类的功能,Swift 团队也使用 protocol 改进 Swift 标准库。
添加以下代码:
let numbers = [10, 20, 30, 40, 50, 60]
let slice = numbers[1...3]
let reversedSlice = slice.reversed()
let answer = reversedSlice.map({ $0 * 10 })
print(answer)
上述代码中的slice
是ArraySlice<Int>
类型,而非Array<Int>
类型。该包装类型提供了一种快速、高效的方式操作数组的一部分。reversedSlice
是ReversedCollection<ArraySlice<Int>>
类型,也是对数组的一种包装。
map
函数是在Sequence
协议extension中实现的,所有Collection
类型都遵守了Sequence
协议。因此,可以在Array
、ReversedCollection
中使用map
函数,且使用过程中没有区别。
10. 查找最高分
目前,已经有多种类型遵守Bird
协议。下面添加以下代码到 playground:
class Motorcycle {
init(name: String) {
self.name = name
speed = 200.0
}
var name: String
var speed: Double
}
Motorcycle
类与Bird
、Flying
协议无关,其也可以与其他类型竞赛。
为了统一不同类型,需要一个单独竞赛 protocol,如下所示:
// 声明Racer协议,指定竞赛的指标。
protocol Racer {
var speed: Double { get }
}
// 下面类型均遵守了Racer协议,即均可以进行比赛。
extension FlappyBird: Racer {
var speed: Double {
airspeedVelocity
}
}
extension SwiftBird: Racer {
var speed: Double {
airspeedVelocity
}
}
extension Penguin: Racer {
var speed: Double {
42
}
}
extension UnladenSwallow: Racer {
var speed: Double {
canFly ? airspeedVelocity : 0.0
}
}
extension Motorcycle: Racer { }
// 数组中实例均遵守了Racer协议
let racers: [Racer] = [
UnladenSwallow.african,
UnladenSwallow.european,
UnladenSwallow.unknown,
Penguin(name: "King Penguin"),
SwiftBird(version: 5.1),
FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0),
Motorcycle(name: "Giacomo")
]
10.1 单独方法查找
使用以下函数查找速度最快的竞赛者:
/// 查找速度最快的选手
func topSpeed(of racers: [Racer]) -> Double {
racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}
topSpeed(of: racers)
topSpeed(of:)
函数返回最快选手的速度。如果传入数组为空,则返回0.0。执行后其速度是5100。
10.2 范型查找
假设Racers
数量众多,目前只需查找部分参与者的最快速度。那么应修改topSpeed(of:)
函数参数为Sequence
类型,而非数组。如下所示:
// RacersType是范型,需遵守Sequence协议。
// where语句指定Sequence的元素必须遵守Racer协议。
func topSpeed<RacersType: Sequence>(of racers: RacersType) -> Double where RacersType.Iterator.Element == Racer {
racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}
使用以下代码查看指定范围数组元素速度:
topSpeed(of: racers[1...3])
运行后输出42。该函数目前支持所有Sequence
类型,包括ArraySlice
。
10.3 为 Sequence 增加 extension
还可以进一步优化查找topSpeed选手的方法,优化后如下:
// 当Sequence的元素为Racer类型时,为其添加topSpeed方法。
extension Sequence where Iterator.Element == Racer {
func topSpeed() -> Double {
self.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}
}
racers.topSpeed()
racers[1...3].topSpeed()
参照 Swift 标准库的实现,扩展了Sequence
协议,增加了topSpeed()
方法,且只有在Sequence
元素是Racer
类型时可用。
11. 使用协议比较大小
Swift 协议还可以用来比较大小。例如,比较对象是否相等==、大于>和小于<。
添加以下代码:
protocol Score {
var value: Int { get }
}
struct RacingScore: Score {
let value: Int
}
有了Score
协议,后续所有处理都可以根据Score
来进行,无需关注具体类型。
让score可比较就可以很方便的查找到最高分数,更新Score
和RacingScore
如下:
protocol Score: Comparable {
var value: Int { get }
}
struct RacingScore: Score {
let value: Int
static func <(lhs: RacingScore, rhs: RacingScore) -> Bool {
lhs.value < rhs.value
}
}
Comparable
协议需要提供小于操作的实现。Swift标准库会根据提供的小于操作,自动实现其他类型的比较操作。
RacingScore(value: 150) >= RacingScore(value: 130) // true
运行后,上述代码打印true
。
12. mutating
截至目前,所有演示都是在增加功能。如何使用 protocol 改变对象的属性呢?可以使用mutating
方法实现,如下所示:
protocol Cheat {
mutating func boost(_ power: Double)
}
Cheat
协议内函数可以修改对象内属性。让SwiftBird
遵守Cheat
协议,如下所示:
extension SwiftBird: Cheat {
// 修改speedFactor,让其增加power。
mutating func boost(_ power: Double) {
speedFactor += power
}
}
修改struct结构体内元素时,函数需使用mutating
标记。
使用以下代码查看boost(_:)
如何工作:
// 创建可变对象
var swiftBird = SwiftBird(version: 5.0)
// 速度增加3
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5015
// 速度再次增加3
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5030
运行后,可以看到SwiftBird
的airspeedVelocity
速度增加了。
总结
现在已经介绍了面向协议编程的优势。通过默认实现,可以为已经存在的协议提供基础功能。这一点类似于继承中的基类,但可用于struct
、enum
。
Demo名称:ProtocolOrientedProgramming
源码地址:https://github.com/pro648/BasicDemos-iOS/tree/master/ProtocolOrientedProgramming
参考资料:
- 面向协议编程与 Cocoa 的邂逅 (上)
- Protocol-Oriented Programming Tutorial in Swift 5.1: Getting Started
- Protocol-Oriented Programming in Swift WWDC2015
- Protocol Oriented Programming is Not a Silver Bullet
欢迎更多指正:https://github.com/pro648/tips
本文地址:https://github.com/pro648/tips/blob/master/sources/面向协议编程.md