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引擎了!

假设您正在开发赛车视频游戏。您希望玩家能够驾驶汽车,骑摩托车和驾驶飞机。他们甚至可以骑不同的鸟(因为这是视频游戏),您可以随心所欲地驾驶!这里的关键是可以驱动或操纵许多不同的“事物”。

此类应用程序的一种常见方法是面向对象的编程,您可以在其中封装所有逻辑,然后将其继承给其他类。基类中将包含“驾驶”和“飞行员”逻辑。

您可以通过为每种车辆创建类来开始对游戏进行编程。现在在鸟概念中使用大头针。稍后您将进行处理。

在编写代码时,您会注意到CarMotorcycle共享一些功能,因此您创建了一个称为MotorVehicle的基类并将其添加到其中。然后,CarMotorcycle将从MotorVehicle继承。您还设计了一个名为Aircraft的基类,Plane继承自该基类。

您认为,“这很好。”可是等等!您的赛车游戏设定为30XX年,有些汽车可以飞行。

现在,您面临困境。 Swift不支持多重继承。您的飞行汽车如何从MotorVehicleAircraft继承?您是否创建另一个合并了两个功能的基类?可能不是,因为没有干净简便的方法可以做到这一点。

谁能从这场灾难性的困境中拯救您的赛车游戏?面向协议的编程可以解救!


Why Protocol-Oriented Programming?

协议允许您将相似的方法,函数和属性分组。 Swift可让您在类,结构和枚举类型上指定这些接口保证。 只有class类型可以使用基类和继承。

Swift中协议的优点是对象可以遵循多种协议。

以这种方式编写应用程序时,您的代码将变得更加模块化。 将协议视为功能的构建块。 通过使对象符合协议来添加新功能时,您无需构建全新的对象。 那很费时间。 而是,添加不同的构造块,直到对象准备就绪为止。

将基类转换为协议可以解决您的视频游戏难题。 使用协议,您可以创建同时符合MotorVehicleAircraftFlyingCar类。 整洁吧?

是时候动手实践一下这个赛车概念了。


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,带有属性namecanFly。 然后,它定义了一个名为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的结构,该结构同时符合BirdFlyable协议。 它的airspeedVelocity是一个包含flappyFrequencyflappyAmplitude的计算属性。 由于不稳定,它会为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
  }
}

企鹅PenguinBird,但它不会飞。 好东西,您没有采用继承方法让所有鸟类都可以飞行(Flyable)

使用协议,您可以定义功能组件并使任何相关对象符合它们。

然后,您声明SwiftBird,但是在我们的游戏中有不同版本的SwiftBirdversion属性越高,由计算属性定义的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,PenguinSwiftBird中删除let canFly =...。 再次构造playground。 您会注意到playground仍然可以成功构建,因为协议扩展现在可以满足该要求。


Enums Can Play, Too

Swift中的枚举Enum类型比CC ++的枚举功能强大得多。 它们采用许多传统上仅支持类或结构类型的功能,这意味着它们可以符合协议。

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符合BirdFlyable这两个协议。 因为它是这样的遵循者,所以它也可以使用canFly的默认实现。


Overriding Default Behavior

您的UnladenSwallow类型通过遵循Bird协议自动收到canFly的实现。 但是,您希望UnladenSwallow.unknowncanFly返回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>。 这种特殊的包装器类型充当原始数组的视图,提供了一种快速有效的方法来对较大数组的各个部分执行操作。 同样,reversedSliceReversedCollection <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子句指定SequenceElement类型必须符合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()函数。 该函数很容易发现,仅在处理SequenceRacer类型时才适用。


Protocol Comparators

Swift协议的另一个功能是如何表示运算符要求,例如==的对象相等,或如何比较><的对象。 您已知道这笔交易-将以下代码添加到playground的底部:

protocol Score {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
}

拥有Score协议意味着您可以编写以相同方式对待所有分数的代码。 但是,通过使用不同的具体类型(例如RacingScore),您不会将这些分数与样式分数或可爱分数混为一谈。 谢谢,编译器!

您希望分数具有可比性,这样您就可以分辨出谁得分最高。 在Swift 3之前,开发人员需要添加全局运算符功能以符合这些协议。 今天,您可以将这些静态方法定义为模型的一部分。 为此,将ScoreRacingScore的定义替换为以下内容:

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,您应该注意到SwiftBirdairspeedVelocity随着每次增强而增加。

要继续学习有关协议的更多信息,请阅读Swift的官方文档official Swift documentation

您可以在Apple的开发人员门户上观看有关面向协议的编程的WWDC精彩会议an excellent WWDC session。 它提供了对所有背后理论的深入探索。

与任何编程范例一样,很容易变得过于旺盛并将其用于所有事物。 克里斯·艾德霍夫(Chris Eidhof)的这篇有趣的博客文章blog post by Chris Eidhof提醒读者,他们应该提防银子弹解决方案。 不要在各处仅因为协议“而使用”。

后记

本篇主要讲述了基于Swift5.1的面向协议编程,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容