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 Monday
case Tuesday
case Wednesday
case Thursday
case Friday
mutating func edit() { ⑩
println("编辑EnumImp...")
self = .Friday ⑪
}
}
var classInstance : Editable = ClassImp()
classInstance.edit()
var structInstance : Editable = StructImp()
structInstance.edit()
var enumInstance : Editable = EnumImp.Monday
enumInstance.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 = 0
var job : String?
var salary : Double = 0
var 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 : String
var firstName : String
var lastName : String
var 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 = firstName
self.lastName = lastName
self.school = school
}
}
class Worker : Person { ⑦
var factory : String
var firstName : String
var lastName : String
var 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 = firstName
self.lastName = lastName
self.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 : String
var firstName : String
var lastName : String
var school : String
var fullName : String {
return self.firstName + "." + self.lastName
}
func description() -> String {
return " firstName: \(firstName)\n lastName: \(lastName)\n
School: \(school)\n Special: \(special)"
}
init (firstName : String, lastName : String, school : String, special : String) {
self.firstName = firstName
self.lastName = lastName
self.school = school
self.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
类型的要求。