Swift编程思想(三) —— 基于Swift5.1的面向协议编
版本记录
版本号 | 时间 |
---|---|
V1.0 | 2020.02.14 星期五 |
前言
Swift作为一门开发语言,它也有自己的特点和对应的编程特点,接下来我们就一起看一下这门语言。让我们一起熟悉和学习它。感兴趣的可以看下面几篇。
1. Swift编程思想(一) —— 函数式编程简介(一)
2. Swift编程思想(二) —— 函数式编程简介(二)
开始
首先看下主要内容
在此面向协议的编程教程中,您将学习
extensions
,默认实现和其他将抽象添加到代码中的技术。
下面看下写作环境
Swift 5, iOS 13, Xcode 11
协议Protocols是Swift的基本功能。 它们在Swift标准库的结构中起着主导作用,并且是一种常见的抽象方法。 它们为某些其他语言提供的接口提供了类似的体验。
本教程将向您介绍称为面向协议的编程(protocol-oriented programming)
的软件工程实践,这已成为Swift的基础。 如果您正在学习Swift,这确实是您需要掌握的东西!
在本教程中,您将了解:
- 面向对象的编程和面向协议的编程之间的区别。
- 具有默认实现的协议。
- 扩展Swift标准库。
- 使用泛型进一步扩展协议。
你在等什么? 是时候启动您的Swift引擎了!
假设您正在开发赛车视频游戏。您希望玩家能够驾驶汽车,骑摩托车和驾驶飞机。他们甚至可以骑不同的鸟(因为这是视频游戏),您可以随心所欲地驾驶!这里的关键是可以驱动或操纵许多不同的“事物”。
此类应用程序的一种常见方法是面向对象的编程,您可以在其中封装所有逻辑,然后将其继承给其他类。基类中将包含“驾驶”和“飞行员”逻辑。
您可以通过为每种车辆创建类来开始对游戏进行编程。现在在鸟概念中使用大头针。稍后您将进行处理。
在编写代码时,您会注意到Car
和Motorcycle
共享一些功能,因此您创建了一个称为MotorVehicle
的基类并将其添加到其中。然后,Car
和Motorcycle
将从MotorVehicle
继承。您还设计了一个名为Aircraft
的基类,Plane
继承自该基类。
您认为,“这很好。”可是等等!您的赛车游戏设定为30XX
年,有些汽车可以飞行。
现在,您面临困境。 Swift
不支持多重继承。您的飞行汽车如何从MotorVehicle
和Aircraft
继承?您是否创建另一个合并了两个功能的基类?可能不是,因为没有干净简便的方法可以做到这一点。
谁能从这场灾难性的困境中拯救您的赛车游戏?面向协议的编程可以解救!
Why Protocol-Oriented Programming?
协议允许您将相似的方法,函数和属性分组。 Swift可让您在类,结构和枚举类型上指定这些接口保证。 只有class
类型可以使用基类和继承。
Swift中协议的优点是对象可以遵循多种协议。
以这种方式编写应用程序时,您的代码将变得更加模块化。 将协议视为功能的构建块。 通过使对象符合协议来添加新功能时,您无需构建全新的对象。 那很费时间。 而是,添加不同的构造块,直到对象准备就绪为止。
将基类转换为协议可以解决您的视频游戏难题。 使用协议,您可以创建同时符合MotorVehicle
和Aircraft
的FlyingCar
类。 整洁吧?
是时候动手实践一下这个赛车概念了。
Hatching the Egg
首先打开Xcode,然后创建一个名为SwiftProtocols.playground
的新playground
。 然后添加以下代码:
protocol Bird {
var name: String { get }
var canFly: Bool { get }
}
protocol Flyable {
var airspeedVelocity: Double { get }
}
使用Command-Shift-Return
建立playground
,以确保其正确编译。
这段代码定义了一个简单的协议Bird
,带有属性name
和canFly
。 然后,它定义了一个名为Flyable
的协议,该协议具有airspeedVelocity
属性。
在以前的协议时代,开发人员将以Flyable
作为基类开始,然后依靠对象继承来定义Bird
和其他任何飞行的事物。
但是在面向协议的编程中,一切都从协议开始。 此技术使您可以封装函数概念,而无需基类。
如您所见,这使整个系统在定义类型时更加灵活。
Defining Protocol-Conforming Types
首先将以下结构定义添加到playground
的底部:
struct FlappyBird: Bird, Flyable {
let name: String
let flappyAmplitude: Double
let flappyFrequency: Double
let canFly = true
var airspeedVelocity: Double {
3 * flappyFrequency * flappyAmplitude
}
}
该代码定义了一个新的名为FlappyBird
的结构,该结构同时符合Bird
和Flyable
协议。 它的airspeedVelocity
是一个包含flappyFrequency
和flappyAmplitude
的计算属性。 由于不稳定,它会为canFly
返回true
。
接下来,将以下两个结构定义添加到playground
的底部
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
}
// Swift is FASTER with each version!
var airspeedVelocity: Double {
version * speedFactor
}
}
企鹅Penguin
是Bird
,但它不会飞。 好东西,您没有采用继承方法让所有鸟类都可以飞行(Flyable)
!
使用协议,您可以定义功能组件并使任何相关对象符合它们。
然后,您声明SwiftBird
,但是在我们的游戏中有不同版本的SwiftBird
。 version
属性越高,由计算属性定义的airspeedVelocity
越快。
但是,您会看到有冗余。 每种类型的Bird
都必须声明其是否可以飞行canFly
-即使系统中已经存在Flyable
的概念。 几乎就像您需要一种定义协议方法的默认实现的方法一样。 嗯,这就是协议扩展的地方。
Extending Protocols With Default Implementations
协议扩展允许您定义协议的默认行为。 要实现第一个,请在Bird
协议定义下面插入以下内容:
extension Bird {
// Flyable birds can fly!
var canFly: Bool { self is Flyable }
}
这段代码定义了Bird
的扩展。 它将canFly
的默认行为设置为在类型符合Flyable
协议时返回true
。 换句话说,任何Flyable
可飞鸟都不再需要显式声明它可以canFly
。 它会像大多数鸟类一样飞翔。
现在从FlappyBird,Penguin
和SwiftBird
中删除let canFly =...
。 再次构造playground
。 您会注意到playground
仍然可以成功构建,因为协议扩展现在可以满足该要求。
Enums Can Play, Too
Swift中的枚举Enum
类型比C
和C ++
的枚举功能强大得多。 它们采用许多传统上仅支持类或结构类型的功能,这意味着它们可以符合协议。
在playground
的末尾添加以下枚举定义:
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
的默认实现。
Overriding Default Behavior
您的UnladenSwallow
类型通过遵循Bird
协议自动收到canFly
的实现。 但是,您希望UnladenSwallow.unknown
为canFly
返回false
。
您可以覆盖默认实现吗? 你打赌 回到playground
的尽头并添加一些新代码:
extension UnladenSwallow {
var canFly: Bool {
self != .unknown
}
}
现在,只有.african
和.european
才能为canFly
返回true
。 试试看! 在playground
的末尾添加以下代码:
UnladenSwallow.unknown.canFly // false
UnladenSwallow.african.canFly // true
Penguin(name: "King Penguin").canFly // false
构建playground
,您会注意到它显示了上面评论中给出的值。
这样,您就可以像在面向对象编程中使用虚拟方法(virtual methods)
那样覆盖属性和方法。
Extending Protocols
您还可以使自己的协议与Swift标准库中的其他协议保持一致,并定义默认行为。 将您的Bird
协议声明替换为以下代码:
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 :["
}
}
符合CustomStringConvertible
意味着您的类型需要具有description
属性,以便在需要时将其自动转换为String
。 您没有定义此属性到当前和将来的每种Bird
类型,而是定义了协议扩展,CustomStringConvertible
仅将其与Bird
类型相关联。
在playground
底部输入以下内容进行尝试:
UnladenSwallow.african
构建playground
,您应该会在助手编辑器中看到I can fly
的字样。 恭喜你! 您已经扩展了协议。
Effects on the Swift Standard Library
协议扩展无法用外壳抓一磅重的椰子,但是您已经知道,它们可以提供一种自定义和扩展命名类型功能的有效方法。 Swift
团队还采用协议来改进Swift标准库。
将此代码添加到playground
的末尾:
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
不是Array <Int>
,而是ArraySlice <Int>
。 这种特殊的包装器类型充当原始数组的视图,提供了一种快速有效的方法来对较大数组的各个部分执行操作。 同样,reversedSlice
是ReversedCollection <ArraySlice <Int >>
,这是另一种包装器类型,具有对原始数组的视图。
幸运的是,开发Swift
标准库的向导将map
函数定义为Sequence
协议的扩展,所有Collection
类型都遵循该协议。 这使您可以像在ReversedCollection
上一样轻松地在Array
上调用map
,而不会注意到差异。 您很快就会借用这一重要的设计模式。
Off to the Races
到目前为止,您已经定义了几种符合Bird
的类型。 现在,您将在playground
的尽头添加完全不同的内容:
class Motorcycle {
init(name: String) {
self.name = name
speed = 200.0
}
var name: String
var speed: Double
}
这个类与鸟类或飞行无关。 您只想将摩托车与企鹅竞赛。 现在该将这些古怪的赛车手带入起跑线了。
Bringing It All Together
为了统一这些不同的类型,您需要一个通用的赛车协议。 得益于一种称为追溯建模(retroactive modeling)
的好主意,您甚至可以在不触及原始模型定义的情况下进行管理。 只需将以下内容添加到您的playground
:
// 1
protocol Racer {
var speed: Double { get } // speed is the only thing racers care about
}
// 2
extension FlappyBird: Racer {
var speed: Double {
airspeedVelocity
}
}
extension SwiftBird: Racer {
var speed: Double {
airspeedVelocity
}
}
extension Penguin: Racer {
var speed: Double {
42 // full waddle speed
}
}
extension UnladenSwallow: Racer {
var speed: Double {
canFly ? airspeedVelocity : 0.0
}
}
extension Motorcycle: Racer {}
// 3
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")]
这是这样做的:
- 1) 首先,定义协议
Racer
。 该协议定义了您的游戏中可以竞争的所有内容。 - 2) 然后,使所有内容符合
Racer
,以便我们所有现有的类型都可以进行比赛。 某些类型(例如Motorcycle
)微不足道。 其他,例如UnladenSwallow
,则需要更多逻辑。 无论哪种方式,当您完成后,都会有许多一致的Racer
类型。 - 3) 在所有类型都位于开始位置的情况下,您现在创建一个
Array <Racer>
,其中包含您所创建的每种类型的实例。
构建playground
检查所有编译。
Top Speed
是时候编写一个确定赛车手最高速度的函数了。 将以下代码添加到playground
的末尾:
func topSpeed(of racers: [Racer]) -> Double {
racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}
topSpeed(of: racers) // 5100
该函数使用Swift标准库函数max
来找到速度最高的赛车并返回。 如果用户为赛车手传入一个空数组,则返回0.0
。
建造playground
,您会发现您先前创建的赛车手的最大速度确实为5100
。
Making It More Generic
假设Racers
相当大,并且您只想找到一部分参与者的最高速度。 解决方案是将topSpeed(of :)
更改为采用Sequence
而不是具体Array
的任何东西。
用以下函数替换现有的topSpeed(of :)
实现:
// 1
func topSpeed<RacersType: Sequence>(of racers: RacersType) -> Double
/*2*/ where RacersType.Iterator.Element == Racer {
// 3
racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}
这可能看起来有点吓人,但是它是如何分解的:
- 1)
RacersType
是此函数的通用类型。 它可以是符合Swift
标准库的Sequence
协议的任何类型。 - 2)
where
子句指定Sequence
的Element
类型必须符合Racer
协议才能使用此功能。 - 3) 实际的函数主体与以前相同。
现在,将以下代码添加到playground
的底部:
topSpeed(of: racers[1...3]) // 42
建立playground
,您将看到输出为42
的答案。 该函数现在适用于任何Sequence
类型,包括ArraySlice
。
Making It More Swifty
这是一个秘密:您可以做得更好。 在`playground的结尾添加以下内容:
extension Sequence where Iterator.Element == Racer {
func topSpeed() -> Double {
self.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}
}
racers.topSpeed() // 5100
racers[1...3].topSpeed() // 42
从Swift
标准库剧本中借用,您现在扩展了Sequence
本身,使其具有topSpeed()
函数。 该函数很容易发现,仅在处理Sequence
的Racer
类型时才适用。
Protocol Comparators
Swift
协议的另一个功能是如何表示运算符要求,例如==
的对象相等,或如何比较>
和<
的对象。 您已知道这笔交易-将以下代码添加到playground
的底部:
protocol Score {
var value: Int { get }
}
struct RacingScore: Score {
let value: Int
}
拥有Score
协议意味着您可以编写以相同方式对待所有分数的代码。 但是,通过使用不同的具体类型(例如RacingScore
),您不会将这些分数与样式分数或可爱分数混为一谈。 谢谢,编译器!
您希望分数具有可比性,这样您就可以分辨出谁得分最高。 在Swift 3
之前,开发人员需要添加全局运算符功能以符合这些协议。 今天,您可以将这些静态方法定义为模型的一部分。 为此,将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
}
}
真好! 您已经将RacingScore
的所有逻辑封装在一个地方。 Comparable
只需要您为小于运算符提供一个实现。 其余要比较的运算符(例如大于)具有Swift标准库基于小于运算符提供的默认实现。
在playground
底部使用以下代码行测试新发现的操作符技能:
RacingScore(value: 150) >= RacingScore(value: 130) // true
建立playground
,您会注意到答案是true
。 您现在可以比较分数了!
Mutating Functions
到目前为止,您实现的每个示例都演示了如何添加函数。 但是,如果您想让协议定义一些可以改变对象外观的东西,该怎么办? 您可以通过在协议中使用可变方法来做到这一点。
在playground
的底部,添加以下新协议:
protocol Cheat {
mutating func boost(_ power: Double)
}
这定义了一种协议,可使您的类型作弊。 怎么样? 通过增加您认为合适的任何东西。
接下来,使用以下代码在SwiftBird
上创建一个符合Cheat
的扩展:
extension SwiftBird: Cheat {
mutating func boost(_ power: Double) {
speedFactor += power
}
}
在这里,您实现boost(_ :)
并通过传入的power
使speedFactor
增加。您添加了mutating
关键字,以使该结构体知道其值之一将在此函数中更改。
将以下代码添加到playground
上,以了解其工作原理:
var swiftBird = SwiftBird(version: 5.0)
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5015
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5030
在这里,您已经创建了一个可变的SwiftBird
,并将其速度提高了三倍,然后又提高了三倍。 构建playground
,您应该注意到SwiftBird
的airspeedVelocity
随着每次增强而增加。
要继续学习有关协议的更多信息,请阅读Swift的官方文档official Swift documentation。
您可以在Apple的开发人员门户上观看有关面向协议的编程的WWDC精彩会议an excellent WWDC session。 它提供了对所有背后理论的深入探索。
与任何编程范例一样,很容易变得过于旺盛并将其用于所有事物。 克里斯·艾德霍夫(Chris Eidhof)的这篇有趣的博客文章blog post by Chris Eidhof提醒读者,他们应该提防银子弹解决方案。 不要在各处仅因为协议“而使用”。
后记
本篇主要讲述了基于Swift5.1的面向协议编程,感兴趣的给个赞或者关注~~~