RainedAllNight‘s Blog

简单聊聊Swift中的Protocol

字数统计: 3.8k阅读时长: 15 min
2018/03/06 Share

Swift中的Protocol

众所周知,Swift是一门面向协议编程(Protocol Oriented Programming 以下简称POP)的语言,其中许多标准库均是基于此来实现的。由于以往使用面向对象的语言的惯性,以至于实际开发中并没有养成面向协议编程的思维习惯。本文将简单来聊聊Swift中的Protocol,以及我们为什么要面向protocol编程,以加深对其的印象和了解。

Swift协议的基本功能

####协议方法
协议可以要求遵循协议的类型实现某些指定的实例方法或类方法。不支持为协议中的方法的参数提供默认值。功能和Objective-C中基本一致

1
2
3
4
5
6
7
8
protocol CoinProtocol {

func tradingPlatform() -> String

func sell()

func buy()
}

如果你想定义为可选方法

1
2
3
4
5
6
7
8
@objc protocol CoinProtocol {

@objc optional func tradingPlatform() -> String

@objc optional func sell()

@objc optional func buy()
}

相比Objective-CSwift中的协议提供了一些更加丰富的功能

####协议属性
协议可以要求遵循协议的类型提供特定名称和类型的实例属性或类型属性,它只指定属性的名称和类型,协议还指定属性是可读的还是可读可写的。

1
2
3
4
protocol CoinProtocol {
var name: String {get}
var price: Double {get set}
}

协议作为类型

协议可以像其他普通类型一样使用,使用场景如下:

  • 作为函数方法的参数或者返回值类型
  • 作为常量变量或者属性的类型
  • 作为集合中元素的类型

代理模式

代理模式,很常用的一种设计模式;不管是Cocoa还是日常开发中都能常看到

协议支持继承、聚合

协议能够继承一个或多个其他协议,可以在继承的协议的基础上增加新的要求。协议的继承语法与类的继承相似,多个被继承的协议间用逗号分隔:

1
2
3
protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
// 这里是协议的定义部分
}

有时候需要同时遵循多个协议,你可以将多个协议采用 SomeProtocol & AnotherProtocol 这样的格式进行组合,称为 协议合成(protocol composition)。你可以罗列任意多个你想要遵循的协议,以与符号(&)分隔。

1
2
3
protocol InheritingProtocol: SomeProtocol & AnotherProtocol {
// 这里是协议的定义部分
}

关联类型(associatedtype)

使用associatedtype来定义一个在协议中使用的关联类型(可以理解为协议中的泛型)
此类型需要在实现协议的类中定义和指明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protocol Unitable {
associatedtype Unit

func calculatingUnit() -> Unit
}

class People: Unitable {
typealias Unit = Int

func calculatingUnit() -> Int {
return 1
}
}

class RMB: Unitable {
typealias Unit = Double

func calculatingUnit() -> Double {
return 1.0
}
}

上面是一个比较简陋的例子,定义了一个单位计算协议,当People类遵循协议时,计算单位为Int,当RMB类遵循协议时,计算单位为Double

通过扩展遵循协议

可以通过扩展类型来遵循协议,可以为已有类型添加方法和属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class BTC {
// ....
}

extension BTC: CoinType {

func tradingPlatform() -> String {
return "Binance"
}

func sell() {
// sell
}

func buy() {
// buy
}
}

协议扩展

协议可以通过扩展来为遵循协议的类型提供属性、方法以及下标的实现。通过这种方式,你可以基于协议本身来实现这些功能,而无需在每个遵循协议的类型中都重复同样的实现,从而达到了协议的默认实现的功能,并且在协议扩展中还可以为协议添加限制条件

1
2
3
4
5
6
7
8
9
10
11
12
13
extension CoinType where Self: BTC {
func tradingPlatform() -> String {
return "default platform"
}

func sell() {
print("sell all coin")
}

func buy() {
print("buy BTC?")
}
}

协议扩展中需要注意的两点是:

1.通过协议扩展为协议要求提供的默认实现和可选的协议要求不同。虽然在这两种情况下,遵循协议的类型都无需自己实现这些要求,但是通过扩展提供的默认实现可以直接调用,而无需使用可选链式调用。

2.如果多个协议扩展都为同一个协议要求提供了默认实现,而遵循协议的类型又同时满足这些协议扩展的限制条件,那么将会使用限制条件最多的那个协议扩展提供的默认实现。


部分摘抄自官方文档,更详细的参见Swift-Protocol

简单介绍完基本概念,我们来看看协议在Swift中的一些基础库中的应用

在讲之前,我们大体可以把标准库中的协议类型分为三种

  • Can do
  • Is a
  • Can be

1.Can do

表示的是协议能够做某件事或者实现某些功能,最常见的一个例子是Hashable,遵循此协议的类型表示具有可hash的功能,这表示你可以得到这个类的整型散列值,把它当做一个字典的Key值等等。这种协议大都以able结尾,这也比较符合它的语义

类似的还有RawRepresentable这个协议,它能够让遵循它的类获得类似于枚举中的初始值的功能,可以从一个原始值来初始化,或者获得类型对象的原始值

其实我们也可以使用基础库的一些协议来实现一些功能,比如使用RawRepresentable来规范和管理Storyboard中的界面跳转
正常情况下我们的segue跳转时一个controller会对应到一个identifier,而这个identifier由于多次使用分散在各处,很容易拼写错误然后导致crash,
可以利用枚举来尝试下解决这个问题

首先我们定义一个Segueable的协议

1
2
3
4
5
6
7
protocol Segueable {
associatedtype CustomSegueIdentifier: RawRepresentable

func performCustomSegue(_ segue: CustomSegueIdentifier, sender: Any?)

func customSegueIdentifier(forSegue segue: UIStoryboardSegue) -> CustomSegueIdentifier
}

定义了一个遵循RawRepresentable协议的关联类型,两个方法,一个跳转的,一个获取identifier的。

我们在扩展中给这两个方法提供下默认实现,顺便约束一下协议实现的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
extension KYXSegueable where Self: UIViewController, CustomSegueIdentifier.RawValue == String {

func performCustomSegue(_ segue: CustomSegueIdentifier, sender: Any?) {
performSegue(withIdentifier: segue.rawValue, sender: sender)
}

func customSegueIdentifier(forSegue segue: UIStoryboardSegue) -> CustomSegueIdentifier {
guard let identifier = segue.identifier, let customSegueIndentifier = CustomSegueIdentifier(rawValue: identifier) else {
fatalError("Cannot get custom segue indetifier for segue: \(segue.identifier ?? "")")
}

return customSegueIndentifier
}
}

我们可以这样使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class SegueTestViewController: UIViewController, KYXSegueable {

typealias CustomSegueIdentifier = SegueType

enum SegueType: String {
case login = "loginSegue"
case regist = "registSegue"
case other = "otherSegue"
}

override func viewDidLoad() {
super.viewDidLoad()

// Do any additional setup after loading the view.
}

@IBAction func handleLoginButtonAction() {
self.performCustomSegue(.login, sender: nil)
}

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
let segueType = self.customSegueIdentifier(forSegue: segue)

switch segueType {
case .login:
print("login")

case .regist:
print("regist")

default:
print("other")
}
}
}

这样我们就可以在提供的关联类型中定义我们跳转的identifier,使用枚举的Switch来匹配和判断不同的跳转

2. Is a

这类在基础库中占大部分,大都基本上以Type结尾,简单可以理解为是某种类型,表明遵守它的类具有某种身份, 拥有这种身份后可以拥有身份所具有的一些特征和功能。当然一个类型可以拥有多种身份,由于Swift中不支持多继承,使用这种协议可以实现一些多继承的场景。

常见的如ErrorType,表明当前类型具有可出现Error的身份,也就相应的具有处理error的功能和一些Error的特征。

值得注意的是在Swift3.0之后基础库中所有以Type结尾的 “is a”类型的协议,都统一去除了type字段,如ErrorType变成了ErrorCollectionType变成了Collection,这样也更符合Swift语法简练的特点和理念

3. Can be

可以成为 可以转换成,例如A可以转换成为B,一般以 Convertible结尾。
如常见的CustomStringConvertible,实现以后可以自定义当前类的输出

1
2
3
4
5
6
7
8
9
10
11
12
13
class Rectangle: CustomStringConvertible {
var length = 10
var width = 20

var description: String {
return "\(width * length)"
}

func log() {
print(self)
// 输出面积 200
}
}

再如CustomStringConvertible现在弃用改成了ExpressibleByStringLiteral,实现此协议的类型可以通过字面量的形式赋值初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct People {
var name: String = ""
var age: Int = 0
var gender: Int = 0
}

extension People: ExpressibleByStringLiteral {
typealias StringLiteralType = String

public init(stringLiteral value: String) {
self = People()
self.name = value
}
}

这样我们就可以直接通过字符串(人名)的方式来直接初始化一个People对象了

1
let xiaoming: People = "xiaoming"

或者扩展一下,这样来操作一下

1
2
3
4
5
6
7
8
extension URL: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
guard let url = URL(string: value) else {
preconditionFailure("url transform is failure")
}
self = url
}
}

这样我们就可以直接通过字面量的形式来创建和使用URL了

我们可以根据基础库协议库中的几个大的分类来选择在我们业务开发中使用协议的场景和姿势


那么为什么我们要使用协议呢,或者说使用协议编程能给我们带来什么,能够解决哪些痛点和问题呢,下面我们就来简单的探讨一下

Why is Protocol

举个栗子,有如下的继承关系的一个需求
E843D966-49C9-401C-9CDB-C0A982D03DF6.png

我们使用传统的面向对象的方式去解决这个问题时,思路大都如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14

class Animal {
var age: Int { return 0 }
var gender: Bool { return true } //假设为true为雄性
//.....等其他一些共有特征

func eat() {
print("eat food")
}

func excrete() {
print("lababa")
}
}

定义了一个动物的基类,定义了动物的一些共有属性(动物特征)和一些共有方法(动物行为),我们的子类都要继承于此基类,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class Cat: Animal {
override var age: Int { return 1 }
override var gender: Bool { return false }

var legNum: Int = 4 //四条腿

override func eat() {
print("eat fish")
}


func run() {
print("runing cat")
}

func catchMouse() {
print("捉老鼠")
}
}

class Eagle: Animal {
override var age: Int { return 2 }

var leg: Int = 2 //两条腿
var wing: Int = 2 //两只翅膀

override func eat() {
print("eat meat")
}

func fly() {
print("flying eagle")
}
}

class Shark: Animal {
override var age: Int { return 3 }
override var gender: Bool { return false }

var tooth: Int = 100 //反正很多...

override func eat() {
print("eat other fish")
}

func swim() {
print("swimming shark")
}
}

如上,我们的CatEagleShark分别通过基类的方式获得了基类的属性和一些方法,然后在子类里根据自身扩充一些属性和方法。这么一看确实是没什么问题。

于是接下来园长说,我们动物园的动物太少了,需要新增一批动物,而且还要和原来的一起按照动物的种类来进行合理的分区饲养管理。新增名单为以下几位

76EC9504-C3F5-4B28-9082-BF6EB7949AFC.png

于是我们立马简单明了的按照了动物种类来做了以下区分

02CDBE2B-1618-41D2-B834-252E8FD1BBA9.png

如图我们分别引入了哺乳动物鸟类鱼类这几个细分的基类,来做更加细分的处理,比如这样

1
2
3
4
5
6
7
8
9
//鸟类
class Birds: Animal {
//鸟类的一些特征定义
}

//鸵鸟
class Ostrich: Birds {
//..
}

于是问题就来了,按照如图来进行区分和管理真的可靠吗?我们知道大都哺乳动物是Runable的,但是很抱歉海豚是swim的,而不是Run;鸵鸟是Run的,而不是像大多数鸟类那样是Fly的。我们的前辈们为了能够对真实世界的对象进行建模,发展出了面向对象编程的概念,但是这套理念有一些缺陷。虽然我们努力用这套抽象和继承的方法进行建模,但是实际的事物往往是一系列特质的组合,而不单单是以一脉相承并逐渐扩展的方式构建的。我们不能在哺乳动物中定义通用的Run方法,因为它并不适用于所有的哺乳动物比如海豚,并且它还可以能适用于其他类型的对象,比如鸵鸟。那么我们怎么才能够在相同的继承关系(但是代码并不通用)和不同的继承关系的对象间共用代码呢。

传统的做法是

1.粘贴/复制:当然这种做法方便快捷,但是方式非常糟糕

2.引入一个基类,在基类中定义通用的属性和方法;这种做法稍微靠谱点,但是基类会变得愈加臃肿,部分子类还会获得一些本身不需要的属性和方法。以后管理起来也是个大包袱

3.多继承:遗憾的是在iOS的世界里并不支持。

4.引入带有相关属性和方法的依赖对象,好像引入额外的依赖也并不是合适的方式

那么我们如何使用面向协议的姿势来解决上面的问题呢

1
2
3
4
5
6
7
8
9
10
11
@objc protocol Runable {
@objc optional func run()
}

@objc protocol Swimable {
@objc optional func swim()
}

@objc protocol Flyable {
@objc optional func fly()
}

我们定义了两个协议,分别是RunableSwimable,具有某种特性的动物只要实现对应的协议就可以拥有其相应的行为。比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//鸟类
class Birds: Animal, Flyable {
//鸟类的一些特征定义
}

//鸵鸟
class Ostrich: Birds, Runable {
func run() {
print("i am ostrich, i can run")
}
}

//老鹰
class Eagle: Birds {
override var age: Int { return 2 }

var leg: Int = 2 //两条腿
var wing: Int = 2 //两只翅膀

override func eat() {
print("eat meat")
}

func fly() {
print("flying eagle")
}
}

再或者我们做的更干脆一点,抛掉Animal基类,来定义一个Animal的协议,任何满足此协议的对象都可以理解为是一个Animal,如下

1
2
3
4
5
6
7
@objc protocol Animal {
@objc optional var age: Int { get set }
@objc optional var gender: Bool { get set }

@objc optional func eat()
@objc optional func gender()
}

于是我们上面的代码可以变成这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//鸟类
class Birds: Animal, Flyable {
//鸟类的一些特征定义
}

//鸵鸟
class Ostrich: Birds, Runable {
func run() {
print("i am ostrich, i can run")
}
}

//老鹰
class Eagle: Birds {
var age: Int = 2

var leg: Int = 2 //两条腿
var wing: Int = 2 //两只翅膀

func eat() {
print("eat meat")
}

func fly() {
print("flying eagle")
}
}

以上基本解决了我们面向对象编程时所面临的一些问题,而且具有高度的灵活性和更低的耦合性。

记得下次有新的需求时,先想想用Protocol来实现怎么样?

关于Protocol的更进一步进阶的使用例子请前往喵神的面向协议编程与 Cocoa 的邂逅 (下)


参考:

面向协议编程与 Cocoa 的邂逅 (上)

我从55个Swift标准库协议中学到了什么?

Swift中协议的简单介绍

CATALOG
  1. 1. Swift中的Protocol
    1. 1.1. Swift协议的基本功能
      1. 1.1.0.1. 协议作为类型
      2. 1.1.0.2. 代理模式
      3. 1.1.0.3. 协议支持继承、聚合
      4. 1.1.0.4. 关联类型(associatedtype)
      5. 1.1.0.5. 通过扩展遵循协议
      6. 1.1.0.6. 协议扩展
      7. 1.1.0.7. 1.Can do
      8. 1.1.0.8. 2. Is a
      9. 1.1.0.9. 3. Can be
  2. 1.2. Why is Protocol