Protocols — 協定

Shin Yulin
iOS 雛鳥學飛之路
10 min readDec 7, 2021

--

A protocol defines a blueprint of methods, properties, and other requirements that suit a particular task or piece of functionality. The protocol can then be adopted by a class, structure, or enumeration to provide an actual implementation of those requirements. Any type that satisfies the requirements of a protocol is said to conform to that protocol. -The Swift Programming Language Guide by Apple

協定 (protocols) 只單純提供定義一個藍圖,規定完成某項任務or功能所需要的方法或屬性。

Classes和structs提供物件的資訊,協定則提供物件將會執行的動作。而協定是抽象的,可以使用協定替程式碼抽象化。

Syntax

class 名稱: 父類別, 協定A, 協定 B

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
// class definition goes here
}

Property Requirements

If a protocol requires a property to be gettable and settable, that property requirement can’t be fulfilled by a constant stored property or a read-only computed property. If the protocol only requires a property to be gettable, the requirement can be satisfied by any kind of property… -The Swift Programming Language Guide by Apple

Protocols 無法定義屬性為儲存or計算屬性,只定義屬性的名稱、是否為 實體屬性 或為 型別屬性 。並且可以定義屬性是否為唯獨( { get } )或可讀寫( {get set} )的。

//型別屬性
protocol SomeProtocol {
static var mustBeSettable: Int { get set }
static var doesNotNeedToBeSettable: Int { get }
}
//實例屬性
protocol FullyNamed {
var fullName: String { get }
}
class User: FullyNamed {
var firstName: String?
var lastName: String
init(lastName: String, firstName: String? = nil) {
self.firstName = firstName
self.lastName = lastName
}
var fullName: String {
return (firstName != nil ? firstName! + " " : "") + lastName
}
}
var user = User(lastName: "Shin", firstName: "Elio")print(user.fullName)// Elio Shin"

Method Requirements

定義實體方法或型別方法,並不進行實作,實作是交由給遵循此Protocol的型別來做

可以定義有可變數量的參數 (variadic parameter) 的方法。 不可為方法的參數提供預設值 加上 static 關鍵字,就會使它成為是屬於那個型別本身的方法。

protocol SomeProtocol {
static func someTypeMethod()
}
protocol RandomNumberGenerator {
func random() -> Double
}
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())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \\(generator.random())")
// Prints "And another one: 0.729023776863283"

Self ? self?

self通常是指class 或 struct 中的當前對象,Self表示任何當前的類別。

在 protocol 裡 Self 不僅指的是 實現該協議的型別本身,也包括了這個型別的子類

Protocol 自身不是一個型別,只有當一個物件實現了 protocol 後才有了型別物件。

protocol Copyable {
func copy() -> Self
}
class MyClass: Copyable {
var num = 1
func copy() -> Self {
let result = type(of: self).init()
result.num = num
return result
}
//必須實現
//如果不實現:Constructing an object of class type 'Self' with a metatype value must use a 'required' initializer。錯誤
required init() {
}
}

Mutating Method Requirements

If you mark a protocol instance method requirement as mutating, you don’t need to write the mutating keyword when writing an implementation of that method for a class. The mutating keyword is only used by structures and enumerations... -The Swift Programming Language Guide by Apple

遵循一個包含Mutating的Protocol時,列舉跟結構定義時必須加上mutating關鍵字,而類別定義時則不用加上。

protocol Togglable {
mutating func 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()

Initializer Requirements

You don’t need to mark protocol initializer implementations with the required modifier on classes that are marked with the final modifier, because final classes can’t subclassed. For more about the final modifier... -The Swift Programming Language Guide by Apple

當一個類別遵從一個含有初始化建構器 (init) 無論是指定或是convenient初始化建構器都需要加上 require,確保所有子類別也有定初始化建構器。若類別為final,則不需要添加required 。

protocol SomeProtocol {
init(someParameter: Int)
}
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// initializer implementation goes here
}
}
class SomeSubClass: SomeClass, SomeProtocol {
// 必須同時加上 required 和 override
// required 來自 Someprotocl 的 conformance
// override 來自 SuperClass
required override init() {
// 建構器的內容
}
}

Protocols as Types

  • 大寫開
  • 作為函式、方法或初始化建構器的參數型別或返回值型別。
  • 作為常數、變數或屬性的型別。
  • 作為陣列、字典或其他集合中的元素型別。
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
}
}

Delegation-委託

Delegation is a design pattern that enables a class or structure to hand off (or delegate) some of its responsibilities to an instance of another type. -The Swift Programming Language Guide by Apple

Delegate — 將特定的工作、職責、權利等給予別人,讓他們為你完成。

Delegating protocol — 委託協議

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

表示此Protocol 只能應用於 Reference Type → class (非 struct / enum)

  • AnyObject / any 差別?

表示此Protocol 只能應用於 Reference Type → class (非 struct / enum)

  • Any 跟 AnyObject的差別

Any:它可以代表任何型別的類別 (class)、結構 (struct)、列舉 (enum),包括函式和可選型別,基本上可以說是任何東西。

AnyObject:它指的是類別的任何實例。這只在使用參考型別 (reference type) 時,才能派上用場。這就相等於 Objective-C 中等的 ‘id’。

class SnakesAndLadders: DiceGame {
let finalSquare = 25
let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
var square = 0
var board: [Int]
init() {
board = Array(repeating: 0, count: finalSquare + 1)
board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02
board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08
}
weak 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)
}
}
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 += 1
print("Rolled a \\(diceRoll)")
}
func gameDidEnd(_ game: DiceGame) {
print("The game lasted for \\(numberOfTurns) turns")
}
}
let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = tracker
game.play()
// Started a new game of Snakes and Ladders
// The game is using a 6-sided dice
// Rolled a 3
// Rolled a 5
// Rolled a 4
// Rolled a 5
// The game lasted for 4 turns

DiceGameDelegate 協議可以被任意類型遵循,用來追踪 DiceGame 的遊戲過程。

Delegate — 代理對象

weak var delegate: DiceGameDelegate?

To prevent strong reference cycles, delegates are declared as weak references.

  • delegate 為 optional 性質,初始值為nil
  • DiceGamDelegateProtocol 為 class-only (AnyObject) ,delegate 遵從了DiceGameDelegate的實例,delegate 運用在 class 裡面,為 Reference Type,所以需要用 weak 來去避免 references cycle。 ⇒ SnakesAndLadders own DiceGameTracker, DiceGameTracker的func own SnakesAndLadders
let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = tracker
game.play()
// Started a new game of Snakes and Ladders
// The game is using a 6-sided dice
// Rolled a 3
// Rolled a 5
// Rolled a 4
// Rolled a 5
// The game lasted for 4 turns

Adding Protocol Conformance with an Extension

Existing instances of a type automatically adopt and conform to a protocol when that conformance is added to the instance’s type in an extension.

protocol TextRepresentable {
var textualDescription: String { get }
}
extension Dice: TextRepresentable {
var textualDescription: String {
return "A \\(sides)-sided dice"
}
}
extension SnakesAndLadders: TextRepresentable {
var textualDescription: String {
return "A game of Snakes and Ladders with \\(finalSquare) squares"
}
}
let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator()) print(d12.textualDescription) // Prints "A 12-sided dice" print(game.textualDescription) // Prints "A game of Snakes and Ladders with 25 squares"

Conditionally Conforming to a Protocol

extension Array: TextRepresentable where Element: TextRepresentable {
var textualDescription: String {
let itemsAsText = self.map { $0.textualDescription }
return "[" + itemsAsText.joined(separator: ", ") + "]"
}
}
let myDice = [d6, d12]
print(myDice.textualDescription)
// Prints "[A 6-sided dice, A 12-sided dice]"

Array 的 class 只要在存储遵循 TextRepresentable protocol的元素时就遵循 TextRepresentable protocol。

Adopting a Protocol Using a Synthesized Implementation

Swift 提供的 Equatable, Hashable, 跟 Comparable Protocol,使用時不需要再重寫。

Equatable

  • Structures that have only stored properties that conform to the Equatable protocol
  • Enumerations that have only associated types that conform to the Equatable protocol
  • Enumerations that have no associated types
  • When conform to Equatable, provide a method that returns whether the given object and self are equal.
struct Vector3D: Equatable {
var x = 0.0, y = 0.0, z = 0.0
}

let twoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
let anotherTwoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
if twoThreeFour == anotherTwoThreeFour {
print("These two vectors are also equivalent.")
}
// Prints "These two vectors are also equivalent."

enum Status: Equatable {
case onTime
case delayed(minute: Int, second: Int)
case notComing(people: People)
}

struct People: Equatable {
var name: String
}

Hashable — conform equatable

  • For a struct, all its stored properties must conform to Hashable.
  • For an enum, all its associated values must conform to Hashable.
  • When conform to Hashable, provide a method that returns the hash value of self.
/// A point in an x-y coordinate system.
struct GridPoint {
var x: Int
var y: Int
}

extension GridPoint: Hashable {
static func == (lhs: GridPoint, rhs: GridPoint) -> Bool {
return lhs.x == rhs.x && lhs.y == rhs.y
}

func hash(into hasher: inout Hasher) {
hasher.combine(x) // 參考屬性製作 Hash value
hasher.combine(y) // 參考屬性製作 Hash value
}
}

Comparable — conform equatable

enum SkillLevel: Comparable {
case beginner
case intermediate
case expert(stars: Int)
}
var levels = [SkillLevel.intermediate, SkillLevel.beginner,
SkillLevel.expert(stars: 5), SkillLevel.expert(stars: 3)]

for level in levels.sorted(by: <) {
print(level)

}
// Prints "beginner"
// Prints "intermediate"
// Prints "expert(stars: 3)"
// Prints "expert(stars: 5)"

Enum 裡的 beginner / intermediate swift是用什麼邏輯在排序的?
⇒ 宣告時的順序

Collections of Protocol Type

遵從相同Protocol 的物件作為相同Type 存放在Array / Dictionary

let things: [TextRepresentable] = [game, d12]
for thing in things {
print(thing.textualDescription)
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dic
  • thing 為 TextRepresentable 類型 而不是 DiceDiceGame 類型

Protocol Inheritance

可以繼承多個Protocol

protocol PrettyTextRepresentable: TextRepresentable {
var prettyTextualDescription: String { get }
}
//遵從 TextRepresentable & PrettyTextRepresentable 的協定
extension SnakesAndLadders: PrettyTextRepresentable {
var prettyTextualDescription: String {
var output = textualDescription + ":\\n"
for index in 1...finalSquare {
switch board[index] {
case let ladder where ladder > 0:
output += "▲ "
case let snake where snake < 0:
output += "▼ "
default:
output += "○ "
}
}
return output
}
}
print(game.prettyTextualDescription)
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○

任何遵循 PrettyTextRepresentable 的 class 在满足該 protocol 的要求时,也必须满足 TextRepresentable protocol 的要求。

Protocols组合使用 SomeProtocol & AnotherProtocol 的形式。可以列举任意数量的協議,用和符號(&)分開。

下面的例子中,將NamedAged 两个協議按照上述语法组合成一个協議,作为函数参数的類型:

protocol Named {
var name: String { get }
}
protocol Aged {
var age: Int { get }
}
struct Person: Named, Aged {
var name: String
var age: Int
}
func wishHappyBirthday(to celebrator: Named & Aged) {
print("Happy birthday, \\(celebrator.name), you're \\(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(to: birthdayPerson)
// Prints "Happy birthday, Malcolm, you're 21!"

OOP? POP? 蘋果官方宣稱「從核心來看,Swift 其實是協定導向 (Protocol-Oriented) 的程式語言

減少重複的Code、權責分開,把每個元件的功能定義清楚,將重複的部分拉出來統一管理。 有兩種方式可以做到以上的目標,其中一個是利用OOP物件導向的的特性-繼承 (inheritance)來去將會重複的部分放在父類別,再由子類別去繼承,但也因為物件導向的特性,子類別會繼承父類別所有內容;有可能會違反了 SOLID 的 介面隔離原則(Interface Segregation Principle)。

另一種的方式就是使用 Composition Pattern 的方式將功能做成組件,讓需要的模組去組合取用。

Why Protocols?

Protocols vs. Inheritance

👉 In SOLID principle, Interface segregation principle states that: A client should never be forced to implement an interface that it doesn’t use or clients shouldn’t be forced to depend on methods they do not use.

Protocols 解決了 Inheritance 會繼承到父類別連可能不需要的基因也一併繼承,其可能違反了SOLID的 Interface Segregation 介面隔離原則)

Swift2.0 之後 針對 Composition Pattern 提出了方便的工具,Protocol Extensions, 透過 extension 的語法去實作 protocol中定義的 interface,在 conform protocol 的時候,就算不實作這些 interface,編譯器也會給過,因為這些 interface 已經在 extension 中被實作了。

protocol Sound {
func makeSound()
}
extension Sound {
func makeSound() {
print("Wow")
}
}
protocol Flyable {
func fly()
}
extension Flyable {
func fly() {
print("✈️")
}
}
class Airplane: Flyable { }
class Pigeon: Sound, Flyable { }
class Penguin: Sound { }
let pigeon = Pigeon()
pigeon.fly() // prints ✈️
pigeon.makeSound() // prints Wow

Checking for Protocol Conformance

使用 is , as operator 確認 protocol 的一致性

protocol HasArea {
var area: Double { get }
}
////////////////////////////////////////////////////////class Circle: HasArea {
let pi = 3.1415927
var radius: Double
var area: Double { return pi * radius * radius }
init(radius: Double) { self.radius = radius }
}
class Country: HasArea {
var area: Double
init(area: Double) { self.area = area }
}
class Animal {
var legs: Int
init(legs: Int) { self.legs = legs }
}
////////////////////////////////////////////////////////let objects: [AnyObject] = [
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]
////////////////////////////////////////////////////////for object in objects {
if let objectWithArea = object as? HasArea {
print("Area is \\(objectWithArea.area)")
} else {
print("Something that doesn't have an area")
}
}
for object in objects {
if object is HasArea {
print("object is HasArea type")
} else {
print("object is NOT HasArea type")
}
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area
// object is HasArea type
// object is HasArea type
// object is NOT HasArea type

Adding Constraints to Protocol Extensions

extension Collection where Element: Equatable {
func allEqual() -> Bool {
for element in self {
if element != self.first {
return false
}
}
return true
}
}
let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]
print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"

Optional Protocol Requirements

在預設的情況下,Swift中的 protocol內的方法都必須要實作,若想要將方法改成 Optional 性質,可以透過下方式

  1. 使用 @objc 屬性標記

@objc 屬性標記的協議只能被從 Objective-C class 或其他 @objc class 繼承的 class 採用。它們不能被 struct 或 enum 採用。

@objc protocol CounterDataSource {
@objc optional func increment(forCount count: Int) -> Int
@objc optional var fixedIncrement: Int { get }
}
class Counter {
var count = 0
var dataSource: CounterDataSource?
func increment() {
if let amount = dataSource?.increment?(forCount: count) {
count += amount
} else if let amount = dataSource?.fixedIncrement {
count += amount
}
}
}
class ThreeSource: NSObject, CounterDataSource {
let fixedIncrement = 3
}
var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
counter.increment()
print(counter.count)
}
// 3
// 6
// 9
// 12
class TowardsZeroSource: NSObject, CounterDataSource {
func increment(forCount count: Int) -> Int {
if count == 0 {
return 0
} else if count < 0 {
return 1
} else {
return -1
}
}
}
counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
counter.increment()
print(counter.count)
}
// -3
// -2
// -1
// 0
// 0
  1. extension 一個預設實作方法:
protocol Printable {
func canPrint() -> Bool
}
extension Printable {
func canPrint() -> Bool {
return true
}
}

Summary

語法上:

屬性:

  • 透過 { get set } 來指定讀寫的屬性
  • 透過 static / class 來指定型別的屬性

方法:

  • 透過 static / class 來指定class的方法
  • 透過 mutating 來要求實現針對valueType的修改方法

init要求:

  • 在Conform的class中 需要使用 required 修飾,確保其子class 也有。

As a type:

代表某個 instance 所遵從的協議,也可以做成 Array 或 Collection

Delegate 委託設計模式,用來封裝需要被委託的功能

Protocol之間的關係:

可繼承、可同時遵循多個Protocol

透過 is, as? , as! 進行 Conformance 的檢查

在繼承時 使用 class 來表示 該protocol的職能可以被 class 繼承

optional 的 Protocol

使用 optional 修飾 propriety、methods、protocol本身 , 同時所有的 optional 與協議本身都必須要使用 @objc 進行修飾,而且只能被 Objective-C 的 class 或者帶有 @objc 的 class 使用。

extension:

對 instance 使用,讓其遵循某個協議

對 protocol 使用,可再遵循其他協議,增加 protocol 的一致性 或是 提供預設的實作方法

使用 where 來增加限制的條件

--

--

Shin Yulin
iOS 雛鳥學飛之路

一個在前往 iOS 工程世界的視覺工作者