第二十一章 协议
协议定义了一个蓝图,规定了用来实现某一特定任务或者功能的方法,属性,以及其他需要的东西。类,结构体或枚举都可以遵循协议,并为协议定义的这些要求提供具体实现。某个类型能够满足某个协议的要求,就可以说该类型遵循这个协议。
除了遵循协议的类型必须实现的要求外,还可以对协议进行扩展,通过扩展来实现一部分要求或者实现一些附加功能,这样遵循协议的类型就能够使用这些功能。
1. Protocol Syntax (协议语法)
我们可以像定义类,结构体和枚举那样定义一个协议。
// 协议名首字母要大写
protocol SomeProtocol {
// protocol definition goes here
}
某个自定义的类型如结构体,类或者枚举都可以采用一个或多个协议,可以在结构体名称的后面添加上一个自定义的协议,用此来作为该结构体的一部分,如果该结构体采用了多个协议,可以用,
来分开这些协议。
struct SomeStructure: FirstProtocol, AnotherProtocol {
// structure definition goes here
}
某个类用一个父类的情况下,可以在该类的名称后面引用其父类,
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
// class definition goes here
}
2. Property Requirements (属性要求)
一个协议可以要求任何确认的类型以特定的名字或类型来提供一个实例属性或类型属性。这个协议不必指明说这个属性是存储还是计算型属性,仅仅只是指明了该需求的属性名和类型,还指明了这个属性是可读还是可写的。(gettable
,settable
)
如果说这个协议要求属性必须是可读或可写的,那么这个属性要求就不能通过一个常量存储属性或只读的计算属性而得到满足和实现。如果说该协议值是要求属性是可写的那么可以通过任何类型的属性来满足这个协议的要求。如果代码需要的话该属性同样也可以是可写的。
属性要求总是要以变量属性声明定义的并且要加上前缀var
关键字,可读和可写的属性都要在类型的定义里面通过写{get set}
来指明该属性是可读和可写的属性。
protocol SomeProtocol {
// 可读和可写的属性
var mustBeSettable: Int { get set }
// 可写属性
var doesNotNeedToBeSettable: Int { get }
}
如果想要在协议里面添加类型属性要求的时候必须要用static
关键字,如果说这个类型属性要求要在类的实现里面的,同样的要用static
或class
关键字。
protocol AnotherProtocol {
static var someTypeProperty: Int { get set }
}
// 单个实例属性要求的协议
protocol FullyNamed {
var fullName: String { get }
}
这个协议FullyNamed
要求一个确认的类型来提供完全限定的名字,仅仅只是指明了它这个类型FullyNamed来提供一个String类型fullname
的可读性实例属性。下面的例子是一个简单的结构体采用和遵循这个协议。
下面这个结构体Person
就是采用了FullyNamed
这个协议。这个结构体的名字叫Person,代表的是特定的人名,采用了该协议作为结构体定以的第一行的一部分。
Person的这个类有单一的String
类型的存储行属性fullname
,这中类型的属性就满足了协议FullyNamed
的要求,也就是说这个Person现在遵循了这个协议。(如果Person没有满足协议的要求,会出现runtime error的)
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
下面是一个比较复杂的类,该类同样也采用和遵循了FullyNamed
这个协议。该类实现了fullName
属性要求作为一个计算型只读属性。类Starship
的每一个实例存储一个name
和一个可选的prefix
,该fullname属性用这个prefix
的值(如果可选prefix存在一个值),插入到name之前从未为该类Starship创建一个全名。
class Starship: FullyNamed {
var prefix: String?
var name: String
init(name: String, prefix: String? = nil) {
self.name = name
self.prefix = prefix
}
var fullName: String {
// 使用三元运算符在name前插入 从而创建一个fullname
return (prefix != nil ? prefix! + " " : "") + name
}
}
var ncc1701 = Starship(name: "Enterprise", prefix: "USS")
// ncc1701.fullName is "USS Enterprise"
3. Method Requirements (方法要求)
协议可以要求遵循协议的类型实现某些指定的实例方法或类方法。这些方法作为协议的一部分,像普通方法一样放在协议的定义中,但是不需要大括号和方法体。可以在协议中定义具有可变参数的方法,和普通方法的定义方式相同。但是,不支持为协议中的方法的参数提供默认值。
正如属性要求中所述,在协议中定义类方法的时候,总是使用static
关键字作为前缀。当类类型遵循协议时,除了static
关键字,还可以使用class
关键字作为前缀:
// 类型方法定义的协议
protocol SomeProtocol {
static func someTypeMethod()
}
该协议RandomNumberGenerator
并告诉我们这些随机数字是怎么生成的,而是简单的要求这个生成器来提供一个标准的方法来生成一个随机的数字。
// 单一的实例方法要求来定义这个协议
protocol RandomNumberGenerator {
func random() -> Double
}
下面是一个类采用和遵循了这个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)
.truncatingRemainder(dividingBy:m))
return lastRandom / m
}
}
let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// 输出:Here's a random number: 0.3746499199817101
print("And another one: \(generator.random())")
// 输出:And another one: 0.729023776863283
4. Mutating Method Requirements (方法要求的变形)
有些时候是有必要为值类型的实例方法来修改这个实例的。比如结构体和枚举这些都是值类型的,我们可以在方法关键字func
的前面添加上mutating
关键字用来表明这个方法修改它所属的实例以及实例的任意属性的值。整个过程在方法章节中的在实例方法中修改值类型有详细介绍。
如果说我们定义的这个协议实力方法要求一定要修改采用了该协议的任何类型中的实例,那么我们可以用mutating
关键字来标记并作为协议定义的一部分。这样会使结构体或枚举来采用该协议从而满足对方法的要求。
下例定义了协议Togglable
,定义了哟个单一的实例方法要求toggle
,就像这个协议的名称那样方法toggle()
意在切换某种已确定类型的状态。就像修改类型的某个属性那样。
protocol Togglable {
mutating func toggle()
}
如果说我们为结构体或着枚举来实现这个协议Togglable
,那么该协议就要像我们提供一个标记为mutating
的方法toggle()
。下面这个例子定义了一个枚举OnOffSwitch
,该枚举切换两种状态,用来指明了着个枚举成员的两种状态on
和off
,同样的道理要想遵循协议Togglable的要求,就必须要吧这个方法标记为mutating,用来指明了这个值类型的枚举OnOffSwitch允许其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
5. Initializer Requirements (构造器要求)
协议可以要求一个确定的类型来实现某个特殊的构造器,我们可以用正常构造器的写法来作为协议的一部分。但是构造器体里面并没有花括号({}
: curly brace)。
protocol SomeProtocol {
init(someParameter: Int)
}
5.1 Class Implementations of Protocol Initializer Requirements (协议构造器中类实现的要求)
我们可以在一个确定的类中实现一个协议构造器(指定构造器或便利构造器)的要求。在两种类型的构造器的情况下,必须要用required
修饰符来标记该构造器。使用required修饰符可以确保所有子类也必须提供此构造器实现,从而也能符合协议,详情见构造6章节中的必要构造器。
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// initializer implementation goes here
}
}
如果说一个子类从父类里面重写了一个指定构造器,并且满足了某个协议对构造器的要求,那么就要用required
和override
修饰符同时用来标记这个构造器。
protocol SomeProtocol {
init()
}
class SomeSuperClass {
init() {
// initializer implementation goes here
}
}
class SomeSubClass: SomeSuperClass, SomeProtocol {
// "required" from SomeProtocol conformance; "override" from SomeSuperClass
required override init() {
// initializer implementation goes here
}
}
5.2 Failable Initializer Requirements (失败构造器要求)
协议也可以为一个确定的类型定义一个失败构造器要求,详情见构造章节中的失败构造器。一个确定的类型中的失败或非失败构造器可以满足该失败构造器。非失败构造器或者隐式展开的失败构造器可以满足一个非失败构造器的要求。