协议(Protocols)
协议 定义了一个蓝图,规定了用来实现某一特定工作或者功能所必需的方法和属性。类,结构体或枚举类型都可以遵循协议,并提供具体实现来完成协议定义的方法和功能。任意能够满足协议要求的类型被称为 遵循(conform) 这个协议。
- 协议的语法(Protocol Syntax)
- 对属性的规定(Property Requirements)
- 对方法的规定(Method Requirements)
- 对Mutating方法的规定(Mutating Method Requirements)
- 对构造器的规定(Initializer Requirements)
- 协议类型(Protocols as Types)
- 委托(代理)模式(Delegation)
- 在扩展中添加协议成员(Adding Protocol Conformance with an Extension)
- 通过扩展补充协议声明(Declaring Protocol Adoption with an Extension)
- 集合中的协议类型(Collections of Protocol Types)
- 协议的继承(Protocol Inheritance)
- 类专属协议(Class-Only Protocol)
- 协议合成(Protocol Composition)
- 检验协议的一致性(Checking for Protocol Conformance)
- 对可选协议的规定(Optional Protocol Requirements)
- 协议扩展(Protocol Extensions)
1.协议的语法
协议的定义方式与类,结构体,枚举的定义非常相似。
protocol SomeProtocol {
// 协议内容
}
要使类遵循某个协议,需要在类型名称后加上协议名称,中间以冒号 : 分隔,作为类型定义的一部分。遵循多个协议时,各协议之间用逗号 , 分隔。
struct SomeStructure: FirstProtocol, AnotherProtocol {
// 结构体内容
}
如果类在遵循协议的同时拥有父类,应该将父类名放在协议名之前,以逗号分隔。
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
// 类的内容
}
2.对属性的规定
协议可以规定其 遵循者 提供特定名称和类型的 实例属性(instance property) 或 类属性(type property) ,而不指定是 存储型属性(stored property) 还是 计算型属性(calculate property) 。此外还必须指明是只读的还是可读可写的。
如果协议规定属性是可读可写的,那么这个属性不能是常量或只读的计算属性。如果协议只要求属性是只读的(gettable),那个属性不仅可以是只读的,如果你代码需要的话,也可以是可写的。
协议中的通常用var来声明属性,在类型声明后加上 { set get } 来表示属性是可读可写的,只读属性则用 { get} 来表示。
protocol SomeProtocol {
var mustBeSettable : Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
在协议中定义类属性(type property)时,总是使用 static 关键字作为前缀。当协议的遵循者是类时,可以使用 class 或 static 关键字来声明类属性,但是在协议的定义中,仍然要使用 static 关键字。
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
如下所示,这是一个含有一个实例属性要求的协议。
protocol FullyNamed {
var fullName: String { get }
}
FullyNamed 协议除了要求协议的遵循者提供fullName属性外,对协议对遵循者的类型并没有特别的要求。这个协议表示,任何遵循 FullyNamed 协议的类型,都具有一个可读的 String 类型实例属性 fullName 。
下面是一个遵循 FullyNamed 协议的简单结构体。
struct Person: FullyNamed{
var fullName: String
}
let john = Person(fullName: "John Appleseed")
//john.fullName 为 "John Appleseed"
这个例子中定义了一个叫做 Person 的结构体,用来表示具有名字的人。从第一行代码中可以看出,它遵循了 FullyNamed 协议。
Person 结构体的每一个实例都有一个叫做 fullName , String 类型的存储型属性。这正好满足了 FullyNamed 协议的要求,也就意味着,Person 结构体完整的 遵循 了协议。(如果协议要求未被完全满足,在编译时会报错)
下面是一个更为复杂的类,它采用并遵循了 FullyNamed 协议:
class Starship: FullyNamed {
var prefix: String?
var name: String
init(name: String, prefix: String? = nil) {
self.name = name
self.prefix = prefix
}
var fullName: String {
return (prefix != nil ? prefix! + " " : "") + name
}
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise"
Starship类把 fullName 属性实现为只读的计算型属性。每一个 Starship 类的实例都有一个名为 name 的属性和一个名为 prefix 的可选属性。 当 prefix 存在时,将 prefix 插入到 name 之前来为Starship构建 fullName , prefix 不存在时,则将直接用 name 构建 fullName 。
3.对方法的规定
协议可以要求其遵循者实现某些指定的实例方法或类方法。这些方法作为协议的一部分,像普通的方法一样放在协议的定义中,但是不需要大括号和方法体。可以在协议中定义具有可变参数的方法,和普通方法的定义方式相
同。但是在协议的方法定义中,不支持参数默认值。
正如对属性的规定中所说的,在协议中定义类方法的时候,总是使用 static 关键字作为前缀。当协议的遵循者是类的时候,虽然你可以在类的实现中使用 class 或者 static 来实现类方法,但是在协议中声明类方法,仍然要使用 static 关键字。
protocol SomeProtocol {
static func someTypeMethod()
}
下面的例子定义了含有一个实例方法的协议。
protocol RandomNumberGenerator {
func random() -> Double
}
RandomNumberGenerator 协议要求其遵循者必须拥有一个名为 random , 返回值类型为 Double 的实例方法。尽管这里并未指明,但是我们假设返回值在[0,1)区间内。
RandomNumberGenerator 协议并不在意每一个随机数是怎样生成的,它只强调这里有一个随机数生成器。
如下所示,下边的是一个遵循了 RandomNumberGenerator 协议的类。该类实现了一个叫做 线性同余生成器(linear congruential generator) 的伪随机数算法。
class LinearCongruentialGenerator: RandomNumberGenerator {
var lastRandom = 42.0
let m = 139968.0
let a = 3877.0
let c = 29573.0
func random() -> Double {
lastRandom = ((lastRandom * a + c) % m)
return lastRandom / m
}
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// 输出 : "Here's a random number: 0.37464991998171"
print("And another one: \(generator.random())")
// 输出 : "And another one: 0.729023776863283"
4.对Mutating方法的规定
有时需要在方法中改变它的实例。例如,值类型(结构体,枚举)的实例方法中,将 mutating 关键字作为函数的前缀,写在 func 之前,表示可以在该方法中修改它所属的实例及其实例属性的值。
如果你在协议中定义了一个方法旨在改变遵循该协议的实例,那么在协议定义时需要在方法前加 mutating 关键字。这使得结构和枚举遵循协议并满足此方法要求。
注意:
用类实现协议中的 mutating 方法时,不用写 mutating 关键字;用结构体,枚举实现协议中的 mutating 方法时,必须写 mutating 关键字。
如下所示, Togglable 协议含有名为 toggle 的实例方法。根据名称推测, toggle() 方法将通过改变实例属性,来切换遵循该协议的实例的状态。
toggle() 方法在定义的时候,使用 mutating 关键字标记,这表明当它被调用时该方法将会改变协议遵循者实例的状态。
protocol Togglable {
mutating func toggle()
}
当使用 枚举 或 结构体 来实现 Togglable 协议时,需要提供一个带有 mutating 前缀的 toggle 方法。
下面定义了一个名为 OnOffSwitch 的枚举类型。这个枚举类型在两种状态之间进行切换,用枚举成员 On 和 Off 表示。枚举类型的 toggle 方法被标记为 mutating 以满足 Togglable 协议的要求。
enum OnOffSwitch: Togglable {
case Off, On
mutating func toggle() {
switch self {
case Off:
self = On
case On:
self = Off
}
}
}
var lightSwitch = OnOffSwitch.Off
lightSwitch.toggle()
//lightSwitch 现在的值为 .On
5.对构造器的规定
协议可以要求它的遵循者实现指定的构造器。你可以像书写普通的构造器那样,在协议的定义里写下构造器的声明,但不需要写花括号和构造器的实体:
protocol SomeProtocol {
init(someParameter: Int)
}
5.1 协议构造器规定在类中的实现
你可以在遵循该协议的类中实现构造器,并指定其为类的指定构造器(designated initializer)或者便利构造器(convenience initializer)。在这两种情况下,你都必须给构造器实现上"required"修饰符:
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
//构造器实现
}
}
使用 required 修饰符可以保证:所有的遵循该协议的子类,同样能为构造器规定提供一个显式的实现或继承实现。
注意:
如果类已经被标记为 final ,那么不需要在协议构造器的实现中使用 required 修饰符。因为final类不能有子类。
如果一个子类重写了父类的指定构造器,并且该构造器遵循了某个协议的规定,那么该构造器的实现需要被同时标示 required 和 override 修饰符
protocol SomeProtocol {
init()
}
class SomeSuperClass {
init() {
// 构造器的实现
}
}
class SomeSubClass: SomeSuperClass, SomeProtocol {
// 因为遵循协议,需要加上"required"; 因为继承自父类,需要加上"override"
required override init() {
// 构造器实现
}
}
5.2可失败构造器的规定
可以通过给协议 Protocols 中添加可失败构造器 (页 0)来使遵循该协议的类型必须实现该可失败构造器。
如果在协议中定义一个可失败构造器,则在遵顼该协议的类型中必须添加同名同参数的可失败构造器或非可失败
构造器。如果在协议中定义一个非可失败构造器,则在遵循该协议的类型中必须添加同名同参数的非可失败构造器或隐式解析类型的可失败构造器( init! )。
6. 协议类型
尽管协议本身并不实现任何功能,但是协议可以被当做类型来使用。
协议可以像其他普通类型一样使用,使用场景:
- 作为函数、方法或构造器中的参数类型或返回值类型
- 作为常量、变量或属性的类型
- 作为数组、字典或其他容器中的元素类型
注意:
协议是一种类型,因此协议类型的名称应与其他类型(Int,Double,String)的写法相同,使用大写字母开头的驼峰式写法,例( FullyNamed 和 RandomNumberGenerator )如下所示,这个示例中将协议当做类型来使用
class Dice {
let sides: Int
let generator: RandomNumberGenerator
init(sides: Int, generator: RandomNumberGenerator) {
self.sides = sides
self.generator = generator
}
func roll() -> Int {
return Int(generator.random() * Double(sides)) + 1
}
}
例子中定义了一个 Dice 类,用来代表桌游中的拥有N个面的骰子。 Dice 的实例含有 sides 和 generator 两个属性,前者是整型,用来表示骰子有几个面,后者为骰子提供一个随机数生成器。
generator 属性的类型为 RandomNumberGenerator ,因此任何遵循了 RandomNumberGenerator 协议的类型的实例都可以赋值给generator ,除此之外,无其他要求。
Dice 类中也有一个构造器(initializer),用来进行初始化操作。构造器中含有一个名为 generator ,类型为 RandomNumberGenerator 的形参。在调用构造方法时创建 Dice 的实例时,可以传入任何遵循RandomNumberGenerator 协议的实例给generator。
Dice 类也提供了一个名为 roll 的实例方法用来模拟骰子的面值。它先使用 generator 的 random() 方法来创建一个[0,1)区间内的随机数,然后使用这个随机数生成正确的骰子面值。因为generator遵循了 RandomNumberGenerator 协议,因而保证了 random 方法可以被调用。
下面的例子展示了如何使用 LinearCongruentialGenerator 的实例作为随机数生成器创建一个六面骰子:
var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
print("Random dice roll is \(d6.roll())")
}
//输出结果
//Random dice roll is 3
//Random dice roll is 5
//Random dice roll is 4
//Random dice roll is 5
//Random dice roll is 4
总结:这篇就今天就写到协议类型,下篇从委托(代理)模式开始总结。