协议 定义了一个蓝图,规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体或枚举都可以遵循协议,并为协议定义的这些要求提供具体实现。某个类型能够满足某个协议的要求,就可以说该类型遵循这个协议。
除了遵循协议的类型必须实现的要求外,还可以对协议进行扩展,通过扩展来实现一部分要求或者实现一些附加功能,这样遵循协议的类型就能够使用这些功能。
协议语法
协议的定义方式与类、结构体和枚举的定义非常相似:
protocol SomeProtocol { // 这里是协议的定义部分 }
要让自定义类型遵循某个协议,在定义类型时,需要在类型名称后加上协议名称,中间以冒号(:)分隔。遵循多个协议时,各协议之间用逗号(,)分隔:
struct SomeStructure: FirstProtocol, AnotherProtocol { // 这里是结构体的定义部分 }
若是一个类拥有父类,应该将父类名放在遵循的协议名之前,以逗号分隔:
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol { // 这里是类的定义部分 }
属性要求
协议可以要求遵循协议的类型提供特定名称和类型的实例属性或类型属性。协议不指定属性是存储属性还是计算属性,它只指定属性的名称和类型。此外,协议还指定属性是可读的还是可读可写的。
如果协议要求属性是可读可写的,那么该属性不能是常量属性或只读的计算型属性。如果协议只要求属性是可读的,那么该属性不仅可以是可读的,如果代码需要的话,还可以是可写的。
协议总是用 var
关键字来声明变量属性,在类型声明后加上 { set get }
来表示属性是可读可写的,可读属性则用 { get }
来表示:
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
协议除了要求遵循协议的类型提供 fullName
属性外,并没有其他特别的要求。这个协议表示,任何遵循 FullyNamed
的类型,都必须有一个可读的 String
类型的实例属性 fullName
。
下面是一个遵循 FullyNamed
协议的简单结构体:
struct Person: FullyNamed { var fullName: String } let john = Person(fullName: "John Appleseed") // john.fullName 为 "John Appleseed"
这个例子中定义了一个叫做 Person
的结构体,用来表示一个具有名字的人。从第一行代码可以看出,它遵循了 FullyNamed
协议。
Person
结构体的每一个实例都有一个 String
类型的存储型属性 fullName
。这正好满足了 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 为 "USS Enterprise"
Starship
类把 fullName
作为只读的计算属性来实现。每一个 Starship
类的实例都有一个名为 name
的非可选属性和一个名为 prefix
的可选属性。 当 prefix
存在时,计算属性 fullName
会将 prefix
插入到 name
之前,从而得到一个带有 prefix
的 fullName
。
方法要求
协议可以要求遵循协议的类型实现某些指定的实例方法或类方法。这些方法作为协议的一部分,像普通方法一样放在协议的定义中,但是不需要大括号和方法体。可以在协议中定义具有可变参数的方法,和普通方法的定义方式相同。但是,不支持为协议中的方法提供默认参数。
正如属性要求中所述,在协议中定义类方法的时候,总是使用 static
关键字作为前缀。即使在类实现时,类方法要求使用 class
或 static
作为关键字前缀,前面的规则仍然适用:
protocol SomeProtocol { static func someTypeMethod() }
下面的例子定义了一个只含有一个实例方法的协议:
protocol RandomNumberGenerator { func random() -> Double }
RandomNumberGenerator
协议要求遵循协议的类型必须拥有一个名为 random
, 返回值类型为 Double
的实例方法。尽管这里并未指明,但是我们假设返回值是从 0.0 到(但不包括)1.0。
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).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
关键字作为方法的前缀,写在 func
关键字之前,表示可以在该方法中修改它所属的实例以及实例的任意属性的值。这一过程在 在实例方法中修改值类型 章节中有详细描述。
如果你在协议中定义了一个实例方法,该方法会改变遵循该协议的类型的实例,那么在定义协议时需要在方法前加 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
构造器要求
协议可以要求遵循协议的类型实现指定的构造器。你可以像编写普通构造器那样,在协议的定义里写下构造器的声明,但不需要写花括号和构造器的实体:
protocol SomeProtocol { init(someParameter: Int) }
协议构造器要求的类实现
你可以在遵循协议的类中实现构造器,无论是作为指定构造器,还是作为便利构造器。无论哪种情况,你都必须为构造器实现标上 required
修饰符:
class SomeClass: SomeProtocol { required init(someParameter: Int) { // 这里是构造器的实现部分 } }
使用 required
修饰符可以确保所有子类也必须提供此构造器实现,从而也能遵循协议。
关于 required
构造器的更多内容,请参考 必要构造器。
注意
如果类已经被标记为
final
,那么不需要在协议构造器的实现中使用required
修饰符,因为final
类不能有子类。关于final
修饰符的更多内容,请参见 防止重写。
如果一个子类重写了父类的指定构造器,并且该构造器满足了某个协议的要求,那么该构造器的实现需要同时标注 required
和 override
修饰符:
protocol SomeProtocol { init() } class SomeSuperClass { init() { // 这里是构造器的实现部分 } } class SomeSubClass: SomeSuperClass, SomeProtocol { // 因为遵循协议,需要加上 required // 因为继承自父类,需要加上 override required override init() { // 这里是构造器的实现部分 } }
可失败构造器要求
协议还可以为遵循协议的类型定义可失败构造器要求,详见 可失败构造器。
遵循协议的类型可以通过可失败构造器(init?
)或非可失败构造器(init
)来满足协议中定义的可失败构造器要求。协议中定义的非可失败构造器要求可以通过非可失败构造器(init
)或隐式解包可失败构造器(init!
)来满足。
注:
1.protocal 相当于“接口”,定义一组规范,再由具体的类、结构体、枚举型变量实现。
2.协议的方法中没有具体实现,不能有默认参数,默认参数相当于一种实现,同时不允许有函数体body。
3.协议的属性的读取类型为{get set}可读写或{get}只读,协议属性定义必须为var。
4.协议不是类型,不能像类一样初始化,而要创建一个类型来遵守协议。
5.协议,本身可以被当作一个类型来看待,但是和遵守协议实现的类等有所区别。
6.关键字class,定义只能被类实现。
7.实现协议必须实现协议中的所有内容。