翻譯:Swift 5.1中的Protocol面向協議的程式設計教程:從入門到精通

架構師易筋 發表於 2020-11-21

說明

在此面向協議protocol的程式設計教程中,您將學習有關擴充套件extensions,預設實現和其他將抽象新增到程式碼中的技術。

協議是Swift的基本功能。它們在Swift標準庫的結構中起著主導作用,並且是一種常見的抽象方法。它們為某些其他語言提供的介面提供了類似的體驗。

本教程將向您介紹稱為面向協議的程式設計的軟體工程實踐,這已成為Swift的基礎。如果您正在學習Swift,這確實是您需要掌握的東西!

在本教程中,您將瞭解:

  • 物件導向的程式設計和麵向協議的程式設計之間的區別。
  • 具有預設實現的協議。
  • 擴充套件Swift標準庫。
  • 使用泛型進一步擴充套件協議。

你在等什麼?是時候啟動您的Swift引擎了!

1. 入門

假設您正在開發賽車視訊遊戲。您希望玩家能夠駕駛汽車,騎摩托車和駕駛飛機。他們甚至可以騎不同的鳥(因為這是電子遊戲),您可以隨心所欲地駕駛!這裡的關鍵是可以驅動或操縱許多不同的“事物”。

此類應用程式的一種常見方法是物件導向的程式設計,您可以在其中封裝所有邏輯,然後將其繼承給其他類。基本類中將包含“驅動”和“飛行員”邏輯。

您可以通過為每種車輛建立類來開始對遊戲進行程式設計。現在在鳥概念中新增圖釘。您稍後將進行處理。

在編寫程式碼時,您會注意到CarMotorcycle共享一些功能,因此建立了一個名為的基類MotorVehicle並將其新增到其中。Car並Motorcycle再繼承MotorVehicle。您也設計稱為基類Aircraft是Plane從繼承。

您認為,“這很好。” 可是等等!您的賽車遊戲設定為30XX年,有些汽車可以飛行。

現在,您面臨困境。Swift不支援多重繼承。您的飛行汽車如何從MotorVehicle和兩者繼承Aircraft?您是否建立了另一個合併了兩個功能的基類?可能不是,因為沒有乾淨簡便的方法可以做到這一點。

誰能從這場災難性的困境中拯救您的賽車遊戲?面向協議Protocol的程式設計可以解救!
在這裡插入圖片描述

1.1 為什麼要進行面向協議的程式設計?

協議允許您將相似的方法,功能和屬性分組。斯威夫特,您可以指定這些介面的保證class,struct和enum型別。只有class型別可以使用基類和繼承。

Swift中協議的優點是物件可以符合多種協議。

以這種方式編寫應用程式時,您的程式碼將變得更加模組化。將協議視為功能的構建塊。通過使物件符合協議來新增新功能時,不會構建一個全新的物件。那很費時間。而是,新增不同的構造塊,直到物件準備就緒為止。

將基類轉換為協議可以解決您的視訊遊戲難題。使用協議,您可以建立FlyingCar同時符合MotorVehicle和的類Aircraft。整潔吧?

是時候動手操作了,並嘗試了這個賽車概念。
在這裡插入圖片描述

2. 實戰

2.1 首先開啟Xcode,然後建立一個名為SwiftProtocols.playground的新遊樂場。

然後新增以下程式碼:

protocol Bird {
  var name: String { get }
  var canFly: Bool { get }
}

protocol Flyable {
  var airspeedVelocity: Double { get }
}

使用Command-Shift-Return建立遊樂場,以確保其正確編譯。

這段程式碼定義了一個簡單的協議Bird,帶有屬性name和canFly。然後定義一個名為的協議Flyable,該協議具有屬性airspeedVelocity。

在過去的協議時代,開發人員將以Flyable基類作為開始,然後依靠物件繼承來定義Bird和執行其他所有東西。

但是在面向協議的程式設計中,一切都從協議開始。此技術使您可以封裝功能概念,而無需基類。

如您所見,這在定義型別時使整個系統更加靈活。

2.2 定義符合協議的型別

首先struct在運動場的底部新增以下定義:

struct FlappyBird: Bird, Flyable {
  let name: String
  let flappyAmplitude: Double
  let flappyFrequency: Double
  let canFly = true

  var airspeedVelocity: Double {
    3 * flappyFrequency * flappyAmplitude
  }
}

這段程式碼定義了一個新的struct名稱FlappyBird,該名稱同時符合Bird和Flyable協議。它airspeedVelocity是包含flappyFrequency和的計算屬性flappyAmplitude。作為飛揚的,它返回true的canFly。

2.3 接下來,struct在運動場的底部新增以下兩個定義:

struct Penguin: Bird {
  let name: String
  let canFly = false
}

struct SwiftBird: Bird, Flyable {
  var name: String { "Swift \(version)" }
  let canFly = true
  let version: Double
  private var speedFactor = 1000.0
  
  init(version: Double) {
    self.version = version
  }

  // Swift is FASTER with each version!
  var airspeedVelocity: Double {
    version * speedFactor
  }
}

Penguin是Bird,但不能飛行。好東西,您沒有采用繼承方法,而是製作了所有鳥Flyable!

使用協議,您可以定義功能元件並使任何相關物件符合它們。

然後您進行宣告SwiftBird,但是在我們的遊戲中有的不同版本SwiftBird。該version屬性越高,其airspeedVelocity由計算屬性定義的速度越快。

但是,您會看到有冗餘。每種型別的項Bird都必須宣告它是否存在,canFly即使Flyable您的系統中已經存在一個概念。幾乎就像您需要一種定義協議方法的預設實現的方法一樣。嗯,這就是協議擴充套件的用武之地。

2.4 使用預設實現擴充套件協議

協議擴充套件允許您定義協議的預設行為。要實現第一個,請在Bird協議定義下方插入以下內容:

extension Bird {
  // Flyable birds can fly!
  var canFly: Bool { self is Flyable }
}

此程式碼在上定義了副檔名Bird。它將預設行為設定為在型別符合協議時canFly返回。換句話說,任何鳥都不再需要顯式宣告它。它會像大多數鳥類一樣飛翔。trueFlyableFlyablecanFly

現在刪除let canFly = …的FlappyBird,Penguin和SwiftBird。再次建造遊樂場。您會注意到,遊樂場仍然可以成功構建,因為協議擴充套件現在可以滿足該要求。

2.5 列舉也可以玩

EnumSwift中的型別比C和C ++中的列舉功能強大得多。它們採用了僅傳統上class或struct型別支援的許多功能,這意味著它們可以符合協議。

enum在遊樂場的末尾新增以下定義:

enum UnladenSwallow: Bird, Flyable {
  case african
  case european
  case unknown
  
  var name: String {
    switch self {
    case .african:
      return "African"
    case .european:
      return "European"
    case .unknown:
      return "What do you mean? African or European?"
    }
  }
  
  var airspeedVelocity: Double {
    switch self {
    case .african:
      return 10.0
    case .european:
      return 9.9
    case .unknown:
      fatalError("You are thrown from the bridge of death!")
    }
  }
}

通過定義正確的屬性,UnladenSwallow符合兩國的協議Bird和Flyable。因為它是這樣的遵循者,所以它也享有的預設實現canFly。

您是否真的認為涉及的教程airspeedVelocity可以忽略Monty Python參考資料?:]

2.6 覆蓋預設行為

您的UnladenSwallow型別canFly通過遵守Bird協議自動收到了的實現。但是,你要UnladenSwallow.unknown回false了canFly。

您可以覆蓋預設實現嗎?你打賭 回到操場的盡頭,新增一些新程式碼:

extension UnladenSwallow {
  var canFly: Bool {
    self != .unknown
  }
}

現在只有.african和.european返回true的canFly。試試看!在操場的盡頭新增以下程式碼:

UnladenSwallow.unknown.canFly         // false
UnladenSwallow.african.canFly         // true
Penguin(name: "King Penguin").canFly  // false

建造遊樂場,您會注意到它顯示了上面註釋中給出的值。

這樣,您就可以像在物件導向程式設計中使用虛擬方法那樣覆蓋屬性和方法。

2.7 擴充套件協議

您還可以使自己的協議與Swift標準庫中的其他協議保持一致,並定義預設行為。將Bird協議宣告替換為以下程式碼:

protocol Bird: CustomStringConvertible {
  var name: String { get }
  var canFly: Bool { get }
}

extension CustomStringConvertible where Self: Bird {
  var description: String {
    canFly ? "I can fly" : "Guess I'll just sit here :["
  }
}

符合CustomStringConvertible意味著您的型別需要具有description屬性,以便String在需要時將其自動轉換為屬性。Bird您並未定義CustomStringConvertible將僅與型別關聯的協議擴充套件,而不是將此屬性新增到當前和將來的每種型別Bird。

在操場底部輸入以下內容進行嘗試:

UnladenSwallow.african

建立遊樂場,您應該I can fly會在助手編輯器中看到“ ”出現。恭喜你!您已經擴充套件了協議。

2.8 對Swift標準庫的影響

協議擴充套件不能像皮一樣抓一磅重的椰子,但是如您所見,它們可以提供一種自定義和擴充套件命名型別功能的有效方法。Swift團隊還採用協議來改進Swift標準庫。

將此程式碼新增到遊樂場的末尾:

let numbers = [10, 20, 30, 40, 50, 60]
let slice = numbers[1...3]
let reversedSlice = slice.reversed()

let answer = reversedSlice.map { $0 * 10 }
print(answer)

您也許可以猜出答案,但是可能令人驚訝的是所涉及的型別。

例如,slice不是Array而是ArraySlice。這種特殊的包裝器型別充當原始陣列的檢視,提供了一種快速有效的方法來對較大陣列的各個部分執行操作。同樣,reversedSlice是的ReversedCollection<ArraySlice>另一個包裝器型別,具有原始陣列的檢視。

幸運的是,開發Swift標準庫的嚮導將map功能定義為Sequence協議的擴充套件,所有Collection型別都遵循該協議。這樣一來,您map就Array可以像在上一樣輕鬆地進行呼叫,ReversedCollection而不會注意到它們之間的差異。您很快就會借用這一重要的設計模式。

2.9 去比賽

到目前為止,您已經定義了幾種符合的型別Bird。現在,您將在操場的盡頭新增完全不同的內容:

class Motorcycle {
  init(name: String) {
    self.name = name
    speed = 200.0
  }

  var name: String
  var speed: Double
}

這個班級與鳥類或飛行無關。您只想讓摩托車與企鵝競賽。是時候將這些古怪的賽車手帶到起跑線上了。

2.10 彙集全部

為了統一這些不同的型別,您需要一個通用的賽車協議。得益於一種稱為追溯建模的好主意,您甚至可以在不觸及原始模型定義的情況下進行管理。只需將以下內容新增到您的遊樂場:

// 1
protocol Racer {
  var speed: Double { get }  // speed is the only thing racers care about
}

// 2
extension FlappyBird: Racer {
  var speed: Double {
    airspeedVelocity
  }
}

extension SwiftBird: Racer {
  var speed: Double {
    airspeedVelocity
  }
}

extension Penguin: Racer {
  var speed: Double {
    42  // full waddle speed
  }
}

extension UnladenSwallow: Racer {
  var speed: Double {
    canFly ? airspeedVelocity : 0.0
  }
}

extension Motorcycle: Racer {}

// 3
let racers: [Racer] =
  [UnladenSwallow.african,
   UnladenSwallow.european,
   UnladenSwallow.unknown,
   Penguin(name: "King Penguin"),
   SwiftBird(version: 5.1),
   FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0),
   Motorcycle(name: "Giacomo")]

這是這樣做的:

  1. 首先,定義協議Racer。該協議定義了您的遊戲中可以競爭的所有內容。
  2. 然後,Racer使所有內容都符合,以便我們所有現有的型別都可以競爭。某些型別(例如)Motorcycle瑣碎地符合。其他的(例如UnladenSwallow)則需要更多的邏輯。無論哪種方式,當您完成操作時,都會得到一堆符合標準的Racer型別。
  3. 在起始處使用所有型別時,現在建立一個Array,其中包含您所建立的每種型別的例項。

建立操場檢查所有編譯。

2.11 最高速度

是時候編寫一個確定賽車手最高速度的函式了。將以下程式碼新增到遊樂場的末尾:

func topSpeed(of racers: [Racer]) -> Double {
  racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

topSpeed(of: racers) // 5100

此函式使用Swift標準庫函式max查詢速度最高的賽車並返回。如果使用者在中傳入空值Array,則返回0.0 racers。

建立操場,您會看到您先前建立的賽車的最大速度確實是5100。

2.12 使它更通用

假設Racers規模很大,您只想找到一部分參與者的最高速度。解決的辦法是改變topSpeed(of:)以採取任何Sequence不是具體的東西Array。

使用topSpeed(of:)以下功能替換您現有的實現:

// 1
func topSpeed<RacersType: Sequence>(of racers: RacersType) -> Double
    /*2*/ where RacersType.Iterator.Element == Racer {
  // 3
  racers.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
}

這可能看起來有點嚇人,但它是如何分解的:

  1. RacersType是此函式的通用型別。它可以是符合Swift標準庫Sequence協議的任何型別。
  2. 該where子句指定的Element型別Sequence必須符合您的Racer協議才能使用此功能。
  3. 實際的功能主體與以前相同。

現在,將以下程式碼新增到操場的底部:

topSpeed(of: racers[1...3]) // 42

建立遊樂場,您將看到輸出為42的答案。該函式現在適用於任何Sequence型別,包括ArraySlice。

2.13 使它更迅速

這是一個祕密:您可以做得更好。在操場的盡頭新增以下內容:

extension Sequence where Iterator.Element == Racer {
  func topSpeed() -> Double {
    self.max(by: { $0.speed < $1.speed })?.speed ?? 0.0
  }
}

racers.topSpeed()        // 5100
racers[1...3].topSpeed() // 42

從Swift標準庫劇本中借用,您現在已經擴充套件了Sequence自身以具有topSpeed()功能。該功能是很容易發現的,只有當你是在處理一個應用Sequence的Racer型別。

2.14 協議比較器

Swift協議的另一個功能是如何表示操作員要求,例如的物件相等==或如何比較>和的物件<。您知道這筆交易吧–在您的遊樂場底部新增以下程式碼:

protocol Score {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
}

擁有Score協議意味著您可以編寫以相同方式對待所有分數的程式碼。但是,通過使用不同的具體型別(例如)RacingScore,您不會將這些分數與樣式分數或可愛分數混為一談。謝謝,編譯器!

您希望分數具有可比性,以便您可以判斷誰得分最高。在Swift 3之前,開發人員需要新增全域性運算子功能以符合這些協議。今天,您可以將這些靜態方法定義為模型的一部分。通過替換的定義,這樣做Score,並RacingScore有以下情況:

protocol Score: Comparable {
  var value: Int { get }
}

struct RacingScore: Score {
  let value: Int
  
  static func <(lhs: RacingScore, rhs: RacingScore) -> Bool {
    lhs.value < rhs.value
  }
}

真好!您已經將所有邏輯封裝RacingScore在一個地方。Comparable只需要您為小於運算子提供一個實現。其餘要比較的運算子(例如大於),具有Swift標準庫基於小於運算子提供的預設實現。

在遊樂場底部使用以下程式碼行測試新發現的操作員技能:

RacingScore(value: 150) >= RacingScore(value: 130) // true

建立遊樂場,您會注意到答案是true預期的。您現在可以比較分數!

2.15 變異功能

到目前為止,您實現的每個示例都演示瞭如何新增功能。但是,如果您想讓協議定義一些可以改變物件外觀的東西,該怎麼辦?您可以通過在協議中使用變異方法來做到這一點。

在操場的底部,新增以下新協議:

protocol Cheat {
  mutating func boost(_ power: Double)
}

這定義了一種協議,可使您的型別作弊。怎麼樣?通過增加您認為合適的任何東西。

接下來,在擴充套件上建立SwiftBird符合Cheat以下程式碼的擴充套件:

extension SwiftBird: Cheat {
  mutating func boost(_ power: Double) {
    speedFactor += power
  }
}

在這裡,您實現boost(_:)並speedFactor通過power傳入來增加值。您新增了mutating關鍵字以struct使它的值之一將在此函式中更改。

將以下程式碼新增到操場上,以瞭解其工作原理:

var swiftBird = SwiftBird(version: 5.0)
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5015
swiftBird.boost(3.0)
swiftBird.airspeedVelocity // 5030

在這裡,您建立了一個SwiftBird可變的a,並將其速度提高了3,然後又提高了3。建造遊樂場,您應注意,每次提升時airspeedVelocity的SwiftBird都增加了。

3. 然後去哪兒?

使用本教程頂部或底部的下載材料按鈕下載完整的Playground。

至此,您已經通過建立簡單協議並使用協議擴充套件對其進行了擴充套件,從而測試了面向協議程式設計的功能。使用預設實現,您可以為現有協議提供常見和自動的行為。這就像更好地像基類一樣,因為它們也可以應用於struct和enum型別。

您還已經看到協議擴充套件可以擴充套件併為Swift標準庫,Cocoa,Cocoa Touch或任何第三方庫中的協議提供預設行為。

要繼續學習有關協議的更多資訊,請閱讀Swift的官方文件

您可以在Apple的開發人員門戶上觀看有關面向協議程式設計的WWDC精彩會議。它提供了對所有背後理論的深入探索。

在其Swift進化提案中閱讀有關操作符一致性的基本原理。您可能還想了解有關SwiftCollection協議的更多資訊,並學習如何構建自己的協議

與任何程式設計範例一樣,很容易變得過於旺盛並將其用於所有事物。克里斯·艾德霍夫(Chris Eidhof)的這篇有趣的部落格文章提醒讀者,他們應該提防銀彈解決方案。不要在各處僅因為“使用協議”。

參考

https://www.raywenderlich.com/6742901-protocol-oriented-programming-tutorial-in-swift-5-1-getting-started