协议定义了某个特殊的任务或者某个功能片段的蓝图,包括了方法,属性,以及其他的需求。协议可以被类,结构体和枚举遵循,并且为这些需求提供真正的实现。满足协议中这些需求的任何类型被称为遵循了这个协议。
除了让遵循协议的类型实现需求,你也可以对协议进行扩展,并且实现这些需求,或者实现一些其他功能,让遵循了协议的类可以使用。
协议的语法
定义协议和定义类,结构体以及枚举的方式很像:
protocol SomeProtocol {
// protocol definition goes here
}
自定义的类型通过把协议的名字放在类型后面,通过冒号分隔来遵循协议,这是定义的一部分。多个协议可以被列在后面,用逗号分隔:
struct SomeStructure: FirstProtocol, AnotherProtocol {
// structure definition goes here
}
如果类有父类,那么把父类的名字放在协议的名字前面,用逗号分隔:
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
// class definition goes here
}
属性需求
协议可以要求任何遵循的类根据指定的名字和类型提供一个实例属性或者类型属性。协议不会指定属性应该是存储属性还是计算属性而是仅仅指定属性的名字和类型。协议也可以指定每一个属性是可读还是可读写的。
如果协议要求一个属性是可读写的,那么一个存储的常量属性或者一个只读的计算属性将无法满足属性的要求。如果属性要求一个属性是可读的,那么任何属性都可以满足要求,就算属性是可写的也是有效的,只要这样子对你的代码是有用的。
协议中属性的需求总是被声明为变量属性,用var关键字作为前缀。可读写的属性通过在声明之后用写上{ get set }注明,可读的属性通过写上{ get }注明。
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
}
在协议中定义类属性时,总是使用static关键字前缀,即使在类实现这个类属性的时候需要使用class或者static关键字,这个规则也同样适用:
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
下面是一个例子,一个简单的协议,有一个实例属性的要求:
protocol FullyNamed {
var fullName: String { get }
}
协议FullyNamed需要一个类遵循,并且提供一个全名。协议不在乎遵循的类型是什么,只是要求这个遵循的类型必要提供一个和他相关的全名。上述协议就要求所有遵循FullyNamed的类型必须提供一个可读的实例属性,名字叫做FullyNamed,类型是String。
如下例,一个遵循了协议FullyNamed的结构体:
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName is "John Appleseed
例子中定义了一个叫做Person的结构体,可以表示出一个人的名字。他在定义的第一行声明了自己遵循了FullyNamed协议。
每一个Person的实例都有一个存储属性,叫做fullName,类型是String,这样子已经满足了FullyNamed协议,也就是说Person争取的遵循了FullyNamed协议。当然,当协议没有被完全满足的情况下,Swift会有一个编译时错误。
下面说一下更加复杂的例子,它们都遵循了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
这个类实现了fullName属性的需求,它是一个StartShip的可读的计算属性。每一个StartShip类型的实例都肯定有一个name,还可能有prefix。fullName把prefix和name相连作为fullName返回,如火不存在prefix那么值返回name。
方法需求
协议可以要求遵循的给实现是定的实例方法和类方法。这些方法和普通的实例方法和类方法写在协议定义的部分,不需要花括号和具体的实现。
和属性需求一样,在协议中定义类方法的时候需要使用static关键字。即使在类实现的时候需要使用class和static前缀的时候也成立。
protocol SomeProtocol {
static func someTypeMethod()
}
下面是一个定义了一个实例方法的协议的例子:
protocol RandomNumberGenerator {
func random() -> Double
}
协议RandomNumberGenerator希望所有遵循的类都实现实例方法random,并且返回一个Double值。我们假设这个返回值是从0.0到1.0的值,尽管协议里面并没指定。
协议RandomNumberGenerator并没有给出对于随机数产生的猜想,只是简单地表示需要一个简单的随机数生成器,这样就可以使用标准的方法获取随机数。
下面是一个遵循了RandomNumberGenerator协议的实现类。这个类实现了一个伪随机的数字生成器,也就是线性同余生成器:
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).truncatingRemainder(dividingBy: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”
可变方法的需求
有一些时候,一个方法需求需要改变当前实例对象,对于值类型的实例,通过在方 法前加上mutating关键字,表示方法允许修改当前实例以及实例的任何属性。
如果你在协议中定义了一个需要改变遵循类实例的实例方法,那么请在方法前加上mutating关键字。这样就可以让枚举和结构体遵循协议并且满足协议。
NOTE:mutating关键字是适用于结构体和枚举。也就是说,一个协议中有一个带有mutatin关键字的实例方法,使用类去遵循并实现该方法的时候,不需要添加mutatiing关键字。
下面的自己定义了一个叫做Togglable的协议,并有一个实例方法叫做toggle。正如方法的名字所示,方法toggle要改变任何遵循的类型的状态。通常要改变类型的属性。
在协议Togglable的定义中方法toggle被标记了mutating关键字,表示这个方法在被调用的希望可以改变遵循的类型的实例。
protocol Togglable {
mutating func toggle()
}
如果你为了结构体和枚举实现了协议Togglable,那么就需要提供一个同样标记为mutating的方法toggle。
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 is now equal to .on