16.2 协议

在面向对象分析与设计方法学(OOAD)中,可能会有这样的经历:一些类的方法所执行的内容是无法确定的,只能等到它的子类中才能确定下来。例如,几何图形类可以有绘制图形的方法,但是绘制图形方法的具体内容无法确定,这是因为我们不知道绘制的是什么样的几何图形。如图16-1所示,子类矩形有自己的绘制方法,子类圆形也有自己的绘制方法。

几何图形这种类在面向对象分析与设计方法学中称为抽象类,方法称为抽象方法。矩形和圆形是几何图形的子类,它们实现了几何图形的绘制图形的抽象方法。

如果几何图形类中所有的方法都是抽象的,那么在Swift和Objective-C中称为协议(protocol),在Java语言中称为接口,在C++中是纯虚类。

16.2 协议 - 图1

图 16-1 几何图形类图

也就是说,协议是高度抽象的,它只规定绘制图形的抽象方法名(onDraw)、参数列表和返回值等信息,不给出具体的实现。这种抽象方法由遵守该协议的“遵守者”具体实现的过程在Swift和Objective-C中称为遵守协议,在Java中称为实现接口

16.2.1 声明和遵守协议

在Swift中,类、结构体和枚举类型可以声明遵守某个协议,并提供该协议所要求的属性和方法。

协议定义语法如下所示:

  1. protocol 协议名 {
  2. // 协议内容
  3. }

在声明遵守协议时,语法如下所示:

  1. 类型 类型名 : 协议1, 协议2 {
  2. // 遵守协议内容
  3. }

其中类型包括classstructenmu,类型名是我们自己定义的,冒号“:”后是需要遵守的协议。当要遵守多个协议时,各协议之间用逗号“,”隔开。

如果一个类继承父类的同时也要遵守协议,应当把父类放在所有的协议之前,如下所示:

  1. class 类名 : 父类, 协议1, 协议2 {
  2. // 遵守协议内容
  3. }

只有类的定义会有父类和协议混合声明,结构体和枚举是没有父类型的。

具有而言,协议可以要求其遵守者提供实例属性、静态属性、实例方法和静态方法等内容的实现。下面我们重点介绍一下对方法和属性的要求。

16.2.2 协议方法

协议可以要求其遵守者实现某些指定方法,包括实例方法和静态方法。这些方法在协议中被定义,协议方法与普通方法类似,但不支持变长参数和默认值参数,也不需要大括号和方法体。

  1. 实例协议方法

下面先看看实例协议方法定义与实现。以下是示例代码:

  1. protocol Figure {
  2. func onDraw() //定义抽象绘制几何图形 ②
  3. }
  4. class Rectangle : Figure {
  5. func onDraw() {
  6. println("绘制矩形...")
  7. }
  8. }
  9. class Circle : Figure {
  10. func onDraw() {
  11. println("绘制圆形...")
  12. }
  13. }
  14. let rect : Figure = Rectangle()
  15. rect.onDraw()
  16. let circle : Figure = Circle()
  17. circle.onDraw()

上述代码第①行定义了协议Figure,其中代码第②行定义抽象绘制几何图形onDraw方法。从代码中可见,只有方法的声明没有具体实现(没有大括号和方法体)。第③行是定义类Rectangle,它是Figure协议的遵守者,它具体实现了Figure协议规定的onDraw方法,当然它的实现也很简单,只是打印一个字符串"绘制矩形…"

第④行是定义类Circle,它也是Figure协议的遵守者,它也具体实现了Figure协议规定的onDraw方法,当然它的实现也很简单,只是打印一个字符串"绘制圆形…"

第⑤行代码创建Rectangle实例,但是声明类型为Figure,我们可以把协议作为类型使用,rect即便是Figure类型,本质上还是Rectangle实例,所以在第⑥行调用onDraw方法的时候,输出结果是"绘制矩形…"。类似地,代码第⑦行是创建的Circle实例,第⑧行调用onDraw方法,输出"绘制圆形…"

  1. 静态协议方法

在协议中定义静态方法与在类中定义静态方法类似,方法前面要添加class关键字。那么遵守该协议的时候,遵守者静态方法前的关键字是class还是static呢?这与遵守者类型是有关系的:如果是类,关键字就是class;如果是结构体或枚举,关键字就是static

以下是示例代码:

  1. protocol Account {
  2. class func interestBy(amount : Double) -> Double
  3. }
  4. class ClassImp : Account {
  5. class func interestBy(amount : Double) -> Double {
  6. return 0.668 amount
  7. }
  8. }
  9. struct StructImp : Account {
  10. static func interestBy(amount : Double) -> Double {
  11. return 0.668 amount
  12. }
  13. }
  14. enum EnumImp : Account {
  15. static func interestBy(amount : Double) -> Double {
  16. return 0.668 * amount
  17. }
  18. }

上述代码第①行是定义协议Account,第②行是声明协议静态方法interestBy,注意需要在方法前面添加关键字class

第③行代码是定义类ClassImp,它要求遵守Account协议,第④行具体实现静态协议方法interestBy,注意方法前面的关键字只能是class

第⑤行代码是定义结构体StructImp,它要求遵守Account协议,第⑥行具体实现静态协议方法interestBy,注意方法前面的关键字只能是static

第⑦行代码是定义枚举EnumImp,它要求遵守Account协议,第⑧行具体实现静态协议方法interestBy,注意方法前面的关键字只能是static

静态协议方法定义和声明都比较麻烦,要与具体的类型有关,使用的时候需要注意。

  1. 变异方法

在结构体和枚举类型中可以定义变异方法,而在类中没有这种方法。原因是结构体和枚举类型中的属性是不可以修改的,通过定义变异方法,可以在变异方法中修改这些属性。而类是引用类型,不需要变异方法就可以修改自己的属性。

在协议定义变异方法时,方法前面要添加mutating关键字。类、结构体和枚举类型都可以实现变异方法,类实现的变异方法时,前面不需要关键字mutating;而结构体和枚举实现变异方法时,前面需要关键字mutating

以下是示例代码:

  1. protocol Editable {
  2. mutating func edit()
  3. }
  4. class ClassImp : Editable {
  5. var name = "ClassImp"
  6. func edit() {
  7. println("编辑ClassImp...")
  8. self.name = "编辑ClassImp..."
  9. }
  10. }
  11. struct StructImp : Editable {
  12. var name = "StructImp"
  13. mutating func edit() {
  14. println("编辑StructImp...")
  15. self.name = "编辑StructImp..."
  16. }
  17. }
  18. enum EnumImp : Editable {
  19. case Monday
  20. case Tuesday
  21. case Wednesday
  22. case Thursday
  23. case Friday
  24. mutating func edit() {
  25. println("编辑EnumImp...")
  26. self = .Friday
  27. }
  28. }
  29. var classInstance : Editable = ClassImp()
  30. classInstance.edit()
  31. var structInstance : Editable = StructImp()
  32. structInstance.edit()
  33. var enumInstance : Editable = EnumImp.Monday
  34. enumInstance.edit()

上述代码第①行是定义协议Editable,第②行是声明协议变异方法edit,注意方法前面添加关键字mutating

第③行代码是定义类ClassImp,它要求遵守Editable协议。第④行具体实现变异方法edit,由于是类遵守该协议,方法前不需要添加关键字mutating。第⑤行是修改当前实例的name属性。在类中,这种修改是允许的。

第⑥行代码是定义结构体StructImp,它要求遵守Editable协议。第⑦行具体实现变异方法edit,方法前需要添加关键字mutating。第⑧行是修改当前实例的name属性,在结构体中修改属性的方法必须是变异的,我们可以尝试将关键字mutating去掉,会发生编译错误。

第⑨行代码是定义枚举EnumImp,它要求遵守Editable协议。第⑩行具体实现变异方法edit,方法前需要添加关键字mutating。第⑪行是修改当前实例的name属性,在结构体中修改属性的方法必须是变异的,否则会发生编译错误。

最后的输出结果如下:

  1. 编辑ClassImp...
  2. 编辑StructImp...
  3. 编辑EnumImp...

16.2.3 协议属性

协议可以要求其遵守者实现某些指定属性,包括实例属性和静态属性,在具体定义的时候,每一种属性都可以有只读和读写之分。

对于遵守者而言,实现属性是非常灵活的。无论是存储属性还是计算属性,只要能满足协议属性的要求,就可以通过编译。甚至是协议中只规定了只读属性,而遵守者提供了对该属性的读写实现,这也是被允许的,因为遵守者满足了协议的只读属性要求。协议只规定了遵守者必须要做的事情,但没有规定不能做的事情。

  1. 实例协议属性

下面先看看实例协议属性定义与实现。示例代码如下:

  1. protocol Person {
  2. var firstName : String { get set }
  3. var lastName : String { get set }
  4. var fullName : String { get }
  5. }
  6. class Employee : Person {
  7. var no : Int = 0
  8. var job : String?
  9. var salary : Double = 0
  10. var firstName : String = "Tony"
  11. var lastName : String = "Guan"
  12. var fullName : String {
  13. get {
  14. return self.firstName + "." + self.lastName
  15. }
  16. set (newFullName) {
  17. var name = newFullName.componentsSeparatedByString(".")
  18. self.firstName = name[0]
  19. self.lastName = name[1]
  20. }
  21. }
  22. }

上述代码第①行是定义协议Person,在该协议中声明了3个属性,其中第②行和第③行属性都是可以读写的,声明时使用getset关键字说明它是可读写的,与普通计算属性相比,getter和setter访问器没有大括号,没有具体实现。代码第④行的fullName属性是只读属性,声明时使用get关键字说明它是只读的。

第⑤行代码定义Employee类,它被要求遵守Person协议,因此需要实现Person协议所规定的3个属性。其中第⑥行代码是实现firstName属性,从定义上看,firstName是存储属性,它事实上实现了Person协议中的var firstName : String { get set }属性规定,否则我们是不能为firstName属性赋值的,也无法获得firstName属性值。第⑦行代码中的lastName属性也是类似的。

第⑧行代码的fullName属性是计算属性,它实现了Person协议中的var fullName : String { get }属性规定。计算属性fullName除了要通过定义getter访问器,实现Person协议只读属性规定外,还定义了setter访问器。Person协议对此没有规定。

  1. 静态协议属性

在协议中定义静态属性与在协议中定义静态属性类似,属性前面要添加class关键字。那么在遵守协议时,遵守者静态属性前面的关键字是class还是static呢?这与遵守者类型是有关系的:如果是类,关键字就是class;如果是结构体或枚举,关键字就是static

以下是示例代码:

  1. protocol Account {
  2. class var interestRate : Double {get} //利率 ②
  3. class func interestBy(amount : Double) -> Double
  4. }
  5. class ClassImp : Account {
  6. class var interestRate : Double {
  7. return 0.668
  8. }
  9. class func interestBy(amount : Double) -> Double {
  10. return ClassImp.interestRate * amount
  11. }
  12. }
  13. struct StructImp : Account {
  14. static var interestRate : Double = 0.668
  15. static func interestBy(amount : Double) -> Double {
  16. return StructImp.interestRate amount
  17. }
  18. }
  19. enum EnumImp : Account {
  20. static var interestRate : Double = 0.668
  21. static func interestBy(amount : Double) -> Double {
  22. return EnumImp.interestRate amount
  23. }
  24. }

上述代码第①行是定义协议Account,第②行是声明协议静态属性interestRate,注意需要在属性前面添加关键字class

第③行代码是定义类ClassImp,它要求遵守Account协议,第④行具体实现静态协议属性interestRate,注意属性前面的关键字只能是class

第⑤行代码是定义结构体StructImp,它要求遵守Account协议,第⑥行具体实现静态协议属性interestRate,注意属性前面的关键字只能是static

第⑦行代码是定义枚举EnumImp,它要求遵守Account协议,第⑧行具体实现静态协议属性interestRate,注意属性前面的关键字只能是static

静态协议属性定义和声明都比较麻烦,要与具体的类型有关,使用的时候需要注意。

16.2.4 把协议作为类型使用

虽然协议没有具体的实现代码,不能被实例化,但它的存在就是为了规范其他类型遵守它实现。在很多人看来,协议并没有什么用途,但事实上协议是非常重要的,它是面向接口编程必不可少的机制,面向接口编程系统的定义与实现应该分离。协议作为数据类型暴露给使用者,使其不用关心具体的实现细节,从而提供系统的可扩展性和可复用性。

在Swift中,协议是作为数据类型使用的,它可以出现在任意允许其他数据类型出现的地方。具体情况请看下面的示例:

  1. protocol Person {
  2. var firstName : String { get set }
  3. var lastName : String { get set }
  4. var fullName : String { get }
  5. func description() -> String
  6. }
  7. class Student : Person {
  8. var school : String
  9. var firstName : String
  10. var lastName : String
  11. var fullName : String {
  12. return self.firstName + "." + self.lastName
  13. }
  14. func description() -> String {
  15. return "firstName: \(firstName) lastName: \(lastName) school: \(school)"
  16. }
  17. init (firstName : String, lastName : String, school : String) {
  18. self.firstName = firstName
  19. self.lastName = lastName
  20. self.school = school
  21. }
  22. }
  23. class Worker : Person {
  24. var factory : String
  25. var firstName : String
  26. var lastName : String
  27. var fullName : String {
  28. return self.firstName + "." + self.lastName
  29. }
  30. func description() -> String {
  31. return "firstName: \(firstName) lastName: \(lastName) factory: \(factory)"
  32. }
  33. init (firstName : String, lastName : String, factory : String) {
  34. self.firstName = firstName
  35. self.lastName = lastName
  36. self.factory = factory
  37. }
  38. }
  39. let student1 : Person = Student(firstName : "Tom", lastName : "Guan", school : "清华大学")
  40. let student2 : Person = Student(firstName : "Ben", lastName : "Guan", school : "北京大学")
  41. let student3 : Person = Student(firstName : "Tony", lastName : "Guan", school : "香港大学")
  42. let worker1 : Person = Worker(firstName : "Tom", lastName : "Zhao", factory : "钢厂")
  43. let worker2 : Person = Worker(firstName : "Ben", lastName : "Zhao", factory : "电厂")
  44. let people : [Person] = [student1, student2, student3, worker1, worker2]
  45. for item : Person in people {
  46. if let student = item as? Student {
  47. println("Student school: \(student.school)")
  48. println("Student fullName: \(student.fullName)")
  49. println("Student description: \(student.description())")
  50. } else if let worker = item as? Worker {
  51. println("Worker factory: \(worker.factory)")
  52. println("Worker fullName: \(worker.fullName)")
  53. println("Worker description: \(worker.description())")
  54. }
  55. }

上述代码第①行定义了协议Person,其中第②行和第③行声明了可读写属性,第④行声明了只读属性,第⑤行声明了方法。

第⑥行定义了Student类,它遵守Person协议。类似地,第⑦行定义了Worker类,它也遵守Person协议。

代码第⑧行和第⑨行创建了3个Student实例,它们的类型是Person协议。代码第⑩行和第⑪行创建了两个Worker实例,它们的类型也是Person协议。然后在第⑫行将这5个实例放入集合people中,people是可以保存Person协议类型的数组。

第⑬行遍历数组people,然后第⑭行使用as操作符进行类型转换,将Person类型转换为Student类型。类似地,第⑮行使用as操作符将Person类型转换为Worker类型。

协议作为类型使用,与其他类型没有区别,不仅可以使用as操作符进行类型转换,还可以使用is操作符进行类型检查。除了不能实例化之外,协议可以像其他类型一样使用。

16.2.5 协议的继承

一个协议继承其他协议就像是类继承一样,图16-2所示是一个继承关系的类。PersonStudent都是协议,Student协议继承了Person协议,而Graduate类遵守Student协议。

16.2 协议 - 图2

图 16-2 协议的继承

下面看具体的示例:

  1. protocol Person {
  2. var firstName : String { get set }
  3. var lastName : String { get set }
  4. var fullName : String { get }
  5. func description() -> String
  6. }
  7. protocol Student : Person {
  8. var school : String { get set }
  9. }
  10. class Graduate : Student {
  11. var special : String
  12. var firstName : String
  13. var lastName : String
  14. var school : String
  15. var fullName : String {
  16. return self.firstName + "." + self.lastName
  17. }
  18. func description() -> String {
  19. return " firstName: \(firstName)\n lastName: \(lastName)\n
  20. School: \(school)\n Special: \(special)"
  21. }
  22. init (firstName : String, lastName : String, school : String, special : String) {
  23. self.firstName = firstName
  24. self.lastName = lastName
  25. self.school = school
  26. self.special = special
  27. }
  28. }
  29. let gStudent = Graduate(firstName : "Tom", lastName : "Guan",
  30. school : "清华大学", special : "计算机")
  31. println(gStudent.description())

上述代码第①行定义了Person协议,第②行定义了Student协议,Student协议继承Person协议,继承的声明与类继承声明一样使用冒号“:”。如果有多个协议需要继承,可以用逗号“,”分隔各个协议。

第③行定义了Graduate类,它遵守Student协议,同时遵守Person协议。代码第④行实例化了Graduate

16.2.6 协议的合成

多个协议可以临时合成一个整体,作为一个类型使用。首先要有一个类型在声明时遵守多个协议。如图16-3所示的轮船协议Ship和武器协议Weapon,它们都声明了一个可读性属性,军舰类WarShip同时遵守了这两个协议。

16.2 协议 - 图3

图 16-3 遵守多个协议

下面看具体的示例:

  1. //定义轮船协议
  2. protocol Ship {
  3. //排水量
  4. var displacement : Double { get set }
  5. }
  6. //定义武器协议
  7. protocol Weapon {
  8. //火炮门数
  9. var gunNumber : Int { get set }
  10. }
  11. //定义军舰类
  12. class WarShip : Ship, Weapon {
  13. //排水量
  14. var displacement = 1000_000.00
  15. //火炮门数
  16. var gunNumber = 10
  17. }
  18. func showWarResource(resource: protocol<Ship, Weapon>) {
  19. println("Ship \(resource.displacement) - Weapon \(resource.gunNumber)")
  20. }
  21. let ship = WarShip()
  22. showWarResource(ship)

上述代码第①行是定义轮船协议Ship,代码第②行是定义武器协议Weapon,代码第③行是定义军舰类,它遵守ShipWeapon

代码第④行定义函数showWarResource,其中参数为protocol类型,这种类型的参数要同时遵守ShipWeapon协议。这种类型就是协议合成,它是一种临时的类型,当作用域结束时,这个类型就不会存在了。代码第⑤行中的参数resource可以访问displacementgunNumber属性。

showWarResource函数是在代码第⑥行调用的,它的参数是WarShip类的实例,它能够满足protocol类型的要求。