Swift-Protocols協(xié)議

協(xié)議 定義了一個藍圖,規(guī)定了用來實現(xiàn)某一特定工作或者功能所必需的方法和屬性。類,結構體或枚舉類型都可以遵循協(xié)議,并提供具體實現(xiàn)來完成協(xié)議定義的方法和功能。任意能夠滿足協(xié)議要求的類型被稱為 遵循(conform) 這個協(xié)議。
除了遵循協(xié)議的類型必須實現(xiàn)那些指定的規(guī)定以外,還可以對協(xié)議進行擴展,實現(xiàn)一些特殊的規(guī)定或者一些附加的功能,使得遵循的類型能夠收益。

協(xié)議的語法

協(xié)議的定義方式與類,結構體,枚舉的定義非常相似。

protocol SomeProtocol {
  // 協(xié)議內容
}

要使類遵循某個協(xié)議,需要在類型名稱后加上協(xié)議名稱,中間以冒號 : 分隔,作為類型定義的一部分。遵循多個協(xié)議時,各協(xié)議之間用逗號 , 分隔。

struct SomeStructure: FirstProtocol, AnotherProtocol {
  // 結構體內容
}

如果類在遵循協(xié)議的同時擁有父類,應該將父類名放在協(xié)議名之前,以逗號分隔。

class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
  // 類的內容
}

對屬性的規(guī)定

協(xié)議可以規(guī)定其 遵循者 提供特定名稱和類型的 實例屬性(instance property) 或 類屬性(type property) ,而不用指定是 存儲型屬性(stored property) 還是 計算型屬性(calculate property) 。此外還必須指明是只讀的還是可讀可寫的。
如果協(xié)議規(guī)定屬性是可讀可寫的,那么這個屬性不能是常量或只讀的計算屬性。如果協(xié)議只要求屬性是只讀的(gettable),那個屬性不僅可以是只讀的,如果你代碼需要的話,也可以是可寫的。
協(xié)議中的通常用var來聲明變量屬性,在類型聲明后加上 { set get } 來表示屬性是可讀可寫的,只讀屬性則用 {get } 來表示。

protocol SomeProtocol {
  var mustBeSettable : Int { get set }
  var doesNotNeedToBeSettable: Int { get }
}

在協(xié)議中定義類屬性(type property)時,總是使用 static 關鍵字作為前綴。當協(xié)議的遵循者是類時,可以使用class 或 static 關鍵字來聲明類屬性:

protocol AnotherProtocol {
  static var someTypeProperty: Int { get set }
}

如下所示,這是一個含有一個實例屬性要求的協(xié)議:

protocol FullyNamed {
  var fullName: String { get }
}

FullyNamed 協(xié)議除了要求協(xié)議的遵循者提供全名屬性外,對協(xié)議對遵循者的類型并沒有特別的要求。這個協(xié)議表示,任何遵循 FullyNamed 協(xié)議的類型,都具有一個可讀的 String 類型實例屬性 fullName 。下面是一個遵循 FullyNamed 協(xié)議的簡單結構體:

struct Person: FullyNamed{
  var fullName: String
}
let john = Person(fullName: "John Appleseed")
//john.fullName 為 "John Appleseed"

這個例子中定義了一個叫做 Person 的結構體,用來表示具有名字的人。從第一行代碼中可以看出,它遵循了 FullyNamed 協(xié)議。Person 結構體的每一個實例都有一個 String 類型的存儲型屬性 fullName 。這正好滿足了 FullyNamed 協(xié)議的要求,也就意味著, Person 結構體完整的 遵循 了協(xié)議。(如果協(xié)議要求未被完全滿足,在編譯時會報錯)下面是一個更為復雜的類,它采用并遵循了 FullyNamed 協(xié)議:

class Starship: FullyNamed {
  var prefix: String?
  var name: Stringinit(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 屬性實現(xiàn)為只讀的計算型屬性。每一個 Starship 類的實例都有一個名為 name 的屬性和一個名為 prefix 的可選屬性。 當 prefix 存在時,將 prefix 插入到 name 之前來為Starship構建 fullName , prefix 不存在時,則將直接用 name 構建 fullName 。

對方法的規(guī)定

協(xié)議可以要求其遵循者實現(xiàn)某些指定的實例方法或類方法。這些方法作為協(xié)議的一部分,像普通的方法一樣放在協(xié)議的定義中,但是不需要大括號和方法體。可以在協(xié)議中定義具有可變參數(shù)的方法,和普通方法的定義方式相同。但是在協(xié)議的方法定義中,不支持參數(shù)默認值。
正如對屬性的規(guī)定中所說的,在協(xié)議中定義類方法的時候,總是使用 static 關鍵字作為前綴。當協(xié)議的遵循者是類的時候,你可以在類的實現(xiàn)中使用 class 或者 static 來實現(xiàn)類方法:

protocol SomeProtocol {
  static func someTypeMethod()
}

下面的例子定義了含有一個實例方法的協(xié)議:

protocol RandomNumberGenerator {
  func random() -> Double
}

RandomNumberGenerator 協(xié)議要求其遵循者必須擁有一個名為 random , 返回值類型為 Double 的實例方法。盡管這里并未指明,但是我們假設返回值在[0,1)區(qū)間內。
RandomNumberGenerator 協(xié)議并不在意每一個隨機數(shù)是怎樣生成的,它只強調這里有一個隨機數(shù)生成器。如下所示,下邊的是一個遵循了 RandomNumberGenerator 協(xié)議的類。該類實現(xiàn)了一個叫做線性同余生成器(linearcongruential generator)的偽隨機數(shù)算法。

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"

委托(代理)模式

委托是一種設計模式,它允許 類 或 結構體 將一些需要它們負責的功能 交由(或委托) 給其他的類型的實例。委托模式的實現(xiàn)很簡單: 定義協(xié)議來封裝那些需要被委托的函數(shù)和方法,使其 遵循者 擁有這些被委托的 函數(shù)和方法 。委托模式可以用來響應特定的動作或接收外部數(shù)據源提供的數(shù)據,而無需要知道外部數(shù)據源的類型信息。下面的例子是兩個基于骰子游戲的協(xié)議:

protocol DiceGame {
  var dice: Dice { get }
  func play()
}
protocol DiceGameDelegate {
  func gameDidStart(game: DiceGame)
  func game(game: DiceGame, didStartNewTurnWithDiceRoll diceRoll:Int)
  func gameDidEnd(game: DiceGame)
}

DiceGame 協(xié)議可以在任意含有骰子的游戲中實現(xiàn)。 DiceGameDelegate 協(xié)議可以用來追蹤 DiceGame 的游戲過程。如下所示, SnakesAndLadders 是 Snakes and Ladders游戲的新版本。新版本使用 Dice 作為骰子,并且實現(xiàn)了 DiceGame 和 DiceGameDelegate 協(xié)議,后者用來記錄游戲的過程:

class SnakesAndLadders: DiceGame {
  let finalSquare = 25
  let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
  var square = 0
  var board: [Int]
  init() {
    board = [Int](count: finalSquare + 1, repeatedValue: 0)
    board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
    board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
  }
  var delegate: DiceGameDelegate?
  func play() {
    square = 0
    delegate?.gameDidStart(self)
    gameLoop: while square != finalSquare {
      let diceRoll = dice.roll()
      delegate?.game(self,didStartNewTurnWithDiceRoll: diceRoll)
      switch square + diceRoll {
        case finalSquare:
          break gameLoop
        case let newSquare where newSquare > finalSquare:
          continue gameLoop
        default:
          square += diceRoll
          square += board[square]
       }
  }
  delegate?.gameDidEnd(self)
  }
}

這個版本的游戲封裝到了 SnakesAndLadders 類中,該類遵循了 DiceGame 協(xié)議,并且提供了相應的可讀的 dice 屬性和 play 實例方法。( dice 屬性在構造之后就不再改變,且協(xié)議只要求 dice 為只讀的,因此將 dice 聲明為常量屬性。)
游戲使用 SnakesAndLadders 類的 構造器(initializer) 初始化游戲。所有的游戲邏輯被轉移到了協(xié)議中的 play 方法, play 方法使用協(xié)議規(guī)定的 dice 屬性提供骰子搖出的值。
注意: delegate 并不是游戲的必備條件,因此 delegate 被定義為遵循 DiceGameDelegate 協(xié)議的可選屬性。因為 delegate 是可選值,因此在初始化的時候被自動賦值為 nil 。隨后,可以在游戲中為 delegate 設置適當?shù)闹怠?br> DicegameDelegate 協(xié)議提供了三個方法用來追蹤游戲過程。被放置于游戲的邏輯中,即 play() 方法內。分別在游戲開始時,新一輪開始時,游戲結束時被調用。
因為 delegate 是一個遵循 DiceGameDelegate 的可選屬性,因此在 play() 方法中使用了 可選鏈 來調用委托方法。 若 delegate 屬性為 nil , 則delegate所調用的方法失效,并不會產生錯誤。若 delegate 不為 nil ,則方法能夠被調用
如下所示, DiceGameTracker 遵循了 DiceGameDelegate 協(xié)議:

class DiceGameTracker: DiceGameDelegate {
  var numberOfTurns = 0
  func gameDidStart(game: DiceGame) {
    numberOfTurns = 0
    if game is SnakesAndLadders {
      print("Started a new game of Snakes and Ladders")
    }
    print("The game is using a \(game.dice.sides)-sided dice")
    }
  func game(game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
    ++numberOfTurns
    print("Rolled a \(diceRoll)")
  }
  func gameDidEnd(game: DiceGame) {
    print("The game lasted for \(numberOfTurns) turns")
  }
}

DiceGameTracker 實現(xiàn)了 DiceGameDelegate 協(xié)議規(guī)定的三個方法,用來記錄游戲已經進行的輪數(shù)。 當游戲開始時, numberOfTurns 屬性被賦值為0; 在每新一輪中遞增; 游戲結束后,輸出打印游戲的總輪數(shù)。
gameDidStart 方法從 game 參數(shù)獲取游戲信息并輸出。 game 在方法中被當做 DiceGame 類型而不是 SnakeAndLadders 類型,所以方法中只能訪問 DiceGame 協(xié)議中的成員。當然了,這些方法也可以在類型轉換之后調用。在上例代碼中,通過 is 操作符檢查 game 是否為 SnakesAndLadders 類型的實例,如果是,則打印出相應的內容。
無論當前進行的是何種游戲, game 都遵循 DiceGame 協(xié)議以確保 game 含有 dice 屬性,因此在 gameDidStart(_:) 方法中可以通過傳入的 game 參數(shù)來訪問 dice 屬性,進而打印出 dice 的 sides 屬性的值。
DiceGameTracker 的運行情況,如下所示:

let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = trackergame.play()
// 開始一個新的Snakes and Ladders的游戲
// 游戲使用 6 面的骰子
// 翻轉得到 3
// 翻轉得到 5
// 翻轉得到 4
// 翻轉得到 5
// 游戲進行了 4 輪

文章摘自開發(fā)者手冊

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容