16.2 协议
在面向对象分析与设计方法学(OOAD)中,可能会有这样的经历:一些类的方法所执行的内容是无法确定的,只能等到它的子类中才能确定下来。例如,几何图形类可以有绘制图形的方法,但是绘制图形方法的具体内容无法确定,这是因为我们不知道绘制的是什么样的几何图形。如图16-1所示,子类矩形有自己的绘制方法,子类圆形也有自己的绘制方法。
几何图形这种类在面向对象分析与设计方法学中称为抽象类,方法称为抽象方法。矩形和圆形是几何图形的子类,它们实现了几何图形的绘制图形的抽象方法。
如果几何图形类中所有的方法都是抽象的,那么在Swift和Objective-C中称为协议(protocol),在Java语言中称为接口,在C++中是纯虚类。

图 16-1 几何图形类图
也就是说,协议是高度抽象的,它只规定绘制图形的抽象方法名(onDraw)、参数列表和返回值等信息,不给出具体的实现。这种抽象方法由遵守该协议的“遵守者”具体实现的过程在Swift和Objective-C中称为遵守协议,在Java中称为实现接口。
16.2.1 声明和遵守协议
在Swift中,类、结构体和枚举类型可以声明遵守某个协议,并提供该协议所要求的属性和方法。
协议定义语法如下所示:
protocol 协议名 {// 协议内容}
在声明遵守协议时,语法如下所示:
类型 类型名 : 协议1, 协议2 {// 遵守协议内容}
其中类型包括class、struct和enmu,类型名是我们自己定义的,冒号“:”后是需要遵守的协议。当要遵守多个协议时,各协议之间用逗号“,”隔开。
如果一个类继承父类的同时也要遵守协议,应当把父类放在所有的协议之前,如下所示:
class 类名 : 父类, 协议1, 协议2 {// 遵守协议内容}
只有类的定义会有父类和协议混合声明,结构体和枚举是没有父类型的。
具有而言,协议可以要求其遵守者提供实例属性、静态属性、实例方法和静态方法等内容的实现。下面我们重点介绍一下对方法和属性的要求。
16.2.2 协议方法
协议可以要求其遵守者实现某些指定方法,包括实例方法和静态方法。这些方法在协议中被定义,协议方法与普通方法类似,但不支持变长参数和默认值参数,也不需要大括号和方法体。
- 实例协议方法
下面先看看实例协议方法定义与实现。以下是示例代码:
protocol Figure { ①func onDraw() //定义抽象绘制几何图形 ②}class Rectangle : Figure { ③func onDraw() {println("绘制矩形...")}}class Circle : Figure { ④func onDraw() {println("绘制圆形...")}}let rect : Figure = Rectangle() ⑤rect.onDraw() ⑥let circle : Figure = Circle() ⑦circle.onDraw() ⑧
上述代码第①行定义了协议Figure,其中代码第②行定义抽象绘制几何图形onDraw方法。从代码中可见,只有方法的声明没有具体实现(没有大括号和方法体)。第③行是定义类Rectangle,它是Figure协议的遵守者,它具体实现了Figure协议规定的onDraw方法,当然它的实现也很简单,只是打印一个字符串"绘制矩形…"。
第④行是定义类Circle,它也是Figure协议的遵守者,它也具体实现了Figure协议规定的onDraw方法,当然它的实现也很简单,只是打印一个字符串"绘制圆形…"。
第⑤行代码创建Rectangle实例,但是声明类型为Figure,我们可以把协议作为类型使用,rect即便是Figure类型,本质上还是Rectangle实例,所以在第⑥行调用onDraw方法的时候,输出结果是"绘制矩形…"。类似地,代码第⑦行是创建的Circle实例,第⑧行调用onDraw方法,输出"绘制圆形…"。
- 静态协议方法
在协议中定义静态方法与在类中定义静态方法类似,方法前面要添加class关键字。那么遵守该协议的时候,遵守者静态方法前的关键字是class还是static呢?这与遵守者类型是有关系的:如果是类,关键字就是class;如果是结构体或枚举,关键字就是static。
以下是示例代码:
protocol Account { ①class func interestBy(amount : Double) -> Double ②}class ClassImp : Account { ③class func interestBy(amount : Double) -> Double { ④return 0.668 * amount}}struct StructImp : Account { ⑤static func interestBy(amount : Double) -> Double { ⑥return 0.668 * amount}}enum EnumImp : Account { ⑦static func interestBy(amount : Double) -> Double { ⑧return 0.668 * amount}}
上述代码第①行是定义协议Account,第②行是声明协议静态方法interestBy,注意需要在方法前面添加关键字class。
第③行代码是定义类ClassImp,它要求遵守Account协议,第④行具体实现静态协议方法interestBy,注意方法前面的关键字只能是class。
第⑤行代码是定义结构体StructImp,它要求遵守Account协议,第⑥行具体实现静态协议方法interestBy,注意方法前面的关键字只能是static。
第⑦行代码是定义枚举EnumImp,它要求遵守Account协议,第⑧行具体实现静态协议方法interestBy,注意方法前面的关键字只能是static。
静态协议方法定义和声明都比较麻烦,要与具体的类型有关,使用的时候需要注意。
- 变异方法
在结构体和枚举类型中可以定义变异方法,而在类中没有这种方法。原因是结构体和枚举类型中的属性是不可以修改的,通过定义变异方法,可以在变异方法中修改这些属性。而类是引用类型,不需要变异方法就可以修改自己的属性。
在协议定义变异方法时,方法前面要添加mutating关键字。类、结构体和枚举类型都可以实现变异方法,类实现的变异方法时,前面不需要关键字mutating;而结构体和枚举实现变异方法时,前面需要关键字mutating。
以下是示例代码:
protocol Editable { ①mutating func edit() ②}class ClassImp : Editable { ③var name = "ClassImp"func edit() { ④println("编辑ClassImp...")self.name = "编辑ClassImp..." ⑤}}struct StructImp : Editable { ⑥var name = "StructImp"mutating func edit() { ⑦println("编辑StructImp...")self.name = "编辑StructImp..." ⑧}}enum EnumImp : Editable { ⑨case Mondaycase Tuesdaycase Wednesdaycase Thursdaycase Fridaymutating func edit() { ⑩println("编辑EnumImp...")self = .Friday ⑪}}var classInstance : Editable = ClassImp()classInstance.edit()var structInstance : Editable = StructImp()structInstance.edit()var enumInstance : Editable = EnumImp.MondayenumInstance.edit()
上述代码第①行是定义协议Editable,第②行是声明协议变异方法edit,注意方法前面添加关键字mutating。
第③行代码是定义类ClassImp,它要求遵守Editable协议。第④行具体实现变异方法edit,由于是类遵守该协议,方法前不需要添加关键字mutating。第⑤行是修改当前实例的name属性。在类中,这种修改是允许的。
第⑥行代码是定义结构体StructImp,它要求遵守Editable协议。第⑦行具体实现变异方法edit,方法前需要添加关键字mutating。第⑧行是修改当前实例的name属性,在结构体中修改属性的方法必须是变异的,我们可以尝试将关键字mutating去掉,会发生编译错误。
第⑨行代码是定义枚举EnumImp,它要求遵守Editable协议。第⑩行具体实现变异方法edit,方法前需要添加关键字mutating。第⑪行是修改当前实例的name属性,在结构体中修改属性的方法必须是变异的,否则会发生编译错误。
最后的输出结果如下:
编辑ClassImp...编辑StructImp...编辑EnumImp...
16.2.3 协议属性
协议可以要求其遵守者实现某些指定属性,包括实例属性和静态属性,在具体定义的时候,每一种属性都可以有只读和读写之分。
对于遵守者而言,实现属性是非常灵活的。无论是存储属性还是计算属性,只要能满足协议属性的要求,就可以通过编译。甚至是协议中只规定了只读属性,而遵守者提供了对该属性的读写实现,这也是被允许的,因为遵守者满足了协议的只读属性要求。协议只规定了遵守者必须要做的事情,但没有规定不能做的事情。
- 实例协议属性
下面先看看实例协议属性定义与实现。示例代码如下:
protocol Person { ①var firstName : String { get set } ②var lastName : String { get set } ③var fullName : String { get } ④}class Employee : Person { ⑤var no : Int = 0var job : String?var salary : Double = 0var firstName : String = "Tony" ⑥var lastName : String = "Guan" ⑦var fullName : String { ⑧get {return self.firstName + "." + self.lastName}set (newFullName) {var name = newFullName.componentsSeparatedByString(".")self.firstName = name[0]self.lastName = name[1]}}}
上述代码第①行是定义协议Person,在该协议中声明了3个属性,其中第②行和第③行属性都是可以读写的,声明时使用get和set关键字说明它是可读写的,与普通计算属性相比,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协议对此没有规定。
- 静态协议属性
在协议中定义静态属性与在协议中定义静态属性类似,属性前面要添加class关键字。那么在遵守协议时,遵守者静态属性前面的关键字是class还是static呢?这与遵守者类型是有关系的:如果是类,关键字就是class;如果是结构体或枚举,关键字就是static。
以下是示例代码:
protocol Account { ①class var interestRate : Double {get} //利率 ②class func interestBy(amount : Double) -> Double}class ClassImp : Account { ③class var interestRate : Double { ④return 0.668}class func interestBy(amount : Double) -> Double {return ClassImp.interestRate * amount}}struct StructImp : Account { ⑤static var interestRate : Double = 0.668 ⑥static func interestBy(amount : Double) -> Double {return StructImp.interestRate * amount}}enum EnumImp : Account { ⑦static var interestRate : Double = 0.668 ⑧static func interestBy(amount : Double) -> Double {return EnumImp.interestRate * amount}}
上述代码第①行是定义协议Account,第②行是声明协议静态属性interestRate,注意需要在属性前面添加关键字class。
第③行代码是定义类ClassImp,它要求遵守Account协议,第④行具体实现静态协议属性interestRate,注意属性前面的关键字只能是class。
第⑤行代码是定义结构体StructImp,它要求遵守Account协议,第⑥行具体实现静态协议属性interestRate,注意属性前面的关键字只能是static。
第⑦行代码是定义枚举EnumImp,它要求遵守Account协议,第⑧行具体实现静态协议属性interestRate,注意属性前面的关键字只能是static。
静态协议属性定义和声明都比较麻烦,要与具体的类型有关,使用的时候需要注意。
16.2.4 把协议作为类型使用
虽然协议没有具体的实现代码,不能被实例化,但它的存在就是为了规范其他类型遵守它实现。在很多人看来,协议并没有什么用途,但事实上协议是非常重要的,它是面向接口编程必不可少的机制,面向接口编程系统的定义与实现应该分离。协议作为数据类型暴露给使用者,使其不用关心具体的实现细节,从而提供系统的可扩展性和可复用性。
在Swift中,协议是作为数据类型使用的,它可以出现在任意允许其他数据类型出现的地方。具体情况请看下面的示例:
protocol Person { ①var firstName : String { get set } ②var lastName : String { get set } ③var fullName : String { get } ④func description() -> String ⑤}class Student : Person { ⑥var school : Stringvar firstName : Stringvar lastName : Stringvar fullName : String {return self.firstName + "." + self.lastName}func description() -> String {return "firstName: \(firstName) lastName: \(lastName) school: \(school)"}init (firstName : String, lastName : String, school : String) {self.firstName = firstNameself.lastName = lastNameself.school = school}}class Worker : Person { ⑦var factory : Stringvar firstName : Stringvar lastName : Stringvar fullName : String {return self.firstName + "." + self.lastName}func description() -> String {return "firstName: \(firstName) lastName: \(lastName) factory: \(factory)"}init (firstName : String, lastName : String, factory : String) {self.firstName = firstNameself.lastName = lastNameself.factory = factory}}let student1 : Person = Student(firstName : "Tom", lastName : "Guan", school : "清华大学") ⑧let student2 : Person = Student(firstName : "Ben", lastName : "Guan", school : "北京大学")let student3 : Person = Student(firstName : "Tony", lastName : "Guan", school : "香港大学") ⑨let worker1 : Person = Worker(firstName : "Tom", lastName : "Zhao", factory : "钢厂") ⑩let worker2 : Person = Worker(firstName : "Ben", lastName : "Zhao", factory : "电厂") ⑪let people : [Person] = [student1, student2, student3, worker1, worker2] ⑫for item : Person in people { ⑬if let student = item as? Student { ⑭println("Student school: \(student.school)")println("Student fullName: \(student.fullName)")println("Student description: \(student.description())")} else if let worker = item as? Worker { ⑮println("Worker factory: \(worker.factory)")println("Worker fullName: \(worker.fullName)")println("Worker description: \(worker.description())")}}
上述代码第①行定义了协议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所示是一个继承关系的类。Person和Student都是协议,Student协议继承了Person协议,而Graduate类遵守Student协议。

图 16-2 协议的继承
下面看具体的示例:
protocol Person { ①var firstName : String { get set }var lastName : String { get set }var fullName : String { get }func description() -> String}protocol Student : Person { ②var school : String { get set }}class Graduate : Student { ③var special : Stringvar firstName : Stringvar lastName : Stringvar school : Stringvar fullName : String {return self.firstName + "." + self.lastName}func description() -> String {return " firstName: \(firstName)\n lastName: \(lastName)\nSchool: \(school)\n Special: \(special)"}init (firstName : String, lastName : String, school : String, special : String) {self.firstName = firstNameself.lastName = lastNameself.school = schoolself.special = special}}let gStudent = Graduate(firstName : "Tom", lastName : "Guan",school : "清华大学", special : "计算机") ④println(gStudent.description())
上述代码第①行定义了Person协议,第②行定义了Student协议,Student协议继承Person协议,继承的声明与类继承声明一样使用冒号“:”。如果有多个协议需要继承,可以用逗号“,”分隔各个协议。
第③行定义了Graduate类,它遵守Student协议,同时遵守Person协议。代码第④行实例化了Graduate。
16.2.6 协议的合成
多个协议可以临时合成一个整体,作为一个类型使用。首先要有一个类型在声明时遵守多个协议。如图16-3所示的轮船协议Ship和武器协议Weapon,它们都声明了一个可读性属性,军舰类WarShip同时遵守了这两个协议。

图 16-3 遵守多个协议
下面看具体的示例:
//定义轮船协议protocol Ship { ①//排水量var displacement : Double { get set }}//定义武器协议protocol Weapon { ②//火炮门数var gunNumber : Int { get set }}//定义军舰类class WarShip : Ship, Weapon { ③//排水量var displacement = 1000_000.00//火炮门数var gunNumber = 10}func showWarResource(resource: protocol<Ship, Weapon>) { ④println("Ship \(resource.displacement) - Weapon \(resource.gunNumber)") ⑤}let ship = WarShip()showWarResource(ship) ⑥
上述代码第①行是定义轮船协议Ship,代码第②行是定义武器协议Weapon,代码第③行是定义军舰类,它遵守Ship和Weapon。
代码第④行定义函数showWarResource,其中参数为protocol类型,这种类型的参数要同时遵守Ship和Weapon协议。这种类型就是协议合成,它是一种临时的类型,当作用域结束时,这个类型就不会存在了。代码第⑤行中的参数resource可以访问displacement和gunNumber属性。
showWarResource函数是在代码第⑥行调用的,它的参数是WarShip类的实例,它能够满足protocol类型的要求。
