第二十一章 协议
12. Protocol Composition (协议组合)
有些时候需要在同一时间来请求一个类型来遵循多个协议,所以我们可以把多个协议组合在一起形成一个单一的请求,这个过程我们称之为协议组合。如果说我们定义一个暂时性的本地协议,并且这个协议在一个组合里面请求多个协议的时候,这个时候协议组合并不会定义新的协议类型。
协议组合有这样的一个形式SomeProtocol & AnotherProtocol,随意我们可以添加很多个协议只要符合当前这个协议组合的形式就行,并用&
符号分开这些协议,一个协议组合同样也可以只有一个类类型,用来指明一个请求的父类。
下面这个例子是将两个协议Named和Aged组合成了一个单一的协议组合,来作为函数的参数类型,
// 组合两个协议
protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
struct Person: Named, Aged {
var name: String
var age: Int
}
// 组合协议作为函数的参数类型
func wishHappyBirthday(to celebrator: Named & Aged) {
print("Happy birthday, \(celebrator.name), you're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// 输出:Happy birthday, Malcolm, you're 21!
这个例子同样也定义了一个函数wishHappyBirthday(to:)
这个celebrator的类型参数是Named & Aged
,也就是说任何类型采用和遵循Named和Aged协议,不在乎那一个特定的类型传参数给这个函数,只要它同时遵循Named和Aged协议就行了。上例同样创建了一个新的实例Person,调用birthdayPerson并且传递这个新的实例给wishHappyBirthday(to:)
函数,因为Person同时遵循两个协议,所以本次调用有效,所以函数才会输出。
下面这个例子同样是用一个class类组合起这个Named协议。
class Location {
var latitude: Double
var longitude: Double
init(latitude: Double, longitude: Double) {
self.latitude = latitude
self.longitude = longitude
}
}
class City: Location, Named {
var name: String
init(name: String, latitude: Double, longitude: Double) {
self.name = name
super.init(latitude: latitude, longitude: longitude)
}
}
func beginConcert(in location: Location & Named) {
print("Hello, \(location.name)!")
}
let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
// Prints "Hello, Seattle!"
这个beginConcert(in:)
函数采用的是类型为Location & Named作为参数。也就是说任何Location
的子类都将会遵循这个Named
协议,在这个例子中City
都满足这两个要求。
传递birthdayPerson
给函数beginConcert(in:)
是无效的,因为Person
并不是Location
的子类,类似地,如果我们有一个Location的子类但是这个子类并不遵循Named协议,用某个实例调用函数beginConcert(in:)都将是无效的
13. Checking for Protocol Conformance (检查协议的一致性)
我们可以通过使用is
和as
运算符(在类型转换里介绍过了)来检查协议的一致性。is
运算符用来确认实例是否遵循某个协议,as?
返回一个可选值,实例遵循时,返回该协议类型,反之则返回nil
。
- 如果实例遵循一个协议,
is
运算符则会返回true
,如果不遵循则返回false
。 - 如果实例不遵循某个协议,向下转换
as?
运算符返回的是这个协议类型的可选值(也就是nil
)。 - 向下转换as!运算符,强制性的要求协议类型向下转换,如果说这个向下转换不成功那么则会会触发一个runtime error。
下面的这个例子定义了一个协议HasArea
,该协议有一个单一的可读性Double类型的属性area
。
protocol HasArea {
var area: Double { get }
}
下面的这两个类,同样都遵循了这个协议HasArea
class Circle: HasArea {
let pi = 3.1415927
var radius: Double
var area: Double { return pi * radius * radius }
init(radius: Double) { self.radius = radius }
}
class Country: HasArea {
var area: Double
init(area: Double) { self.area = area }
}
class Animal {
var legs: Int
init(legs: Int) { self.legs = legs }
}
这个类Circle
实现了这个计算型area
属性,该area属性属于计算型属性给予这个变量存储型属性radius
。而这个类Country
直接是以存储型属性的方式实现了这个area的请求。相同的是这两个类都遵循了这个HasArea协议。而这个类Animal,并没用采用协议HasArea。
Circle,Country和Animal这三个不同的类并没有一个共享的基类(shared base class),尽管如此,它们三个都是类类型,所以这三个类型的实例可以用来构造一个用来存储类型为AnyObject的一个数组。
// 实例化一个数组,(创建了一个shared base class)
let objects: [AnyObject] = [
// 该数组包含了三个不同的类和相对应的属性值
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]
所以说现在这个数组objects
可以迭代里面的元素了,看它们是否都遵循了这个协议HasArea
当迭代出的元素符合HasArea协议时,将as?运算符返回的可选值通过可选绑定,绑定到 objectWithArea常量上。objectWithArea是HasArea协议类型的实例,因此area属性可以被访问和打印。objects数组中的元素的类型并不会因为强转而丢失类型信息,它们仍然是 Circle,Country和Animal类型。然而,当它们被赋值给objectWithArea常量时,只被视为 HasArea类型,因此只有area属性能够被访问。
for object in objects {
// 可选绑定和用as?运算符来判断是否遵循某个协议
if let objectWithArea = object as? HasArea {
print("Area is \(objectWithArea.area)")
} else {
print("Something that doesn't have an area")
}
}
输出:
Area is 12.5663708
Area is 243610.0
Something that doesn’t have an area