天天看點

Swift學習第七槍--協定(一)協定(Protocols)

版權聲明:本文為部落客原創文章,未經部落客允許不得轉載。 https://blog.csdn.net/u010046908/article/details/51553279

協定(Protocols)

協定 定義了一個藍圖,規定了用來實作某一特定工作或者功能所必需的方法和屬性。類,結構體或枚舉類型都可以遵循協定,并提供具體實作來完成協定定義的方法和功能。任意能夠滿足協定要求的類型被稱為 遵循(confor

m) 這個協定。

  • 協定的文法(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
           

總結:這篇就今天就寫到協定類型,下篇從委托(代理)模式開始總結。

繼續閱讀