Swift之路 —— 协议

Author Avatar
xiaoLit Created: Aug 28, 2019 Updated: Dec 25, 2019

前言:
学习后整理,大多记录Swift区别于Objective-C的特性,来源官方文档。

一、协议语法

protocol SomeProtocol {
    // 这里是协议的定义部分
}

若是一个类拥有父类,应该将父类名放在遵循的协议名之前,以逗号分隔:

class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
    // 这里是类的定义部分
}

二、属性与方法

protocol SomeProtocol {
    var mustBeSettable: Int { get set }

    static var someTypeProperty: Int { get }

    static func someTypeMethod() -> Int
}

遵守协议:

struct someStruct: SomeProtocol {
    var mustBeSettable: String

    static var someTypeProperty: Int

    static func someTypeMethod() -> Int {
        return 0
    }
}

一般对于协议中的方法会通过扩展写上默认实现,方便用于代理时,代理对象只需要实现需要的方法

extension SomeProtocol {
    static func someTypeMethod() -> Int {
        print("default")
        return 0
    }
}

三、异变

与在实例方法中修改值类型相似,需要关键词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()
// lightSwitch 现在的值为 .on

四、构造器要求

protocol SomeProtocol {
    init(someParameter: Int)
}

使用 required 修饰符可以确保所有子类也必须提供此构造器实现,从而也能遵循协议。

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // 这里是构造器的实现部分
    }
}

注意:
如果类已经被标记为 final,那么不需要在协议构造器的实现中使用 required 修饰符,因为 final 类不能有子类。关于 final 修饰符的更多内容,请参见防止重写

如果一个子类重写了父类的指定构造器,并且该构造器满足了某个协议的要求,那么该构造器的实现需要同时标注 requiredoverride 修饰符:

protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        // 这里是构造器的实现部分
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // 因为遵循协议,需要加上 required
    // 因为继承自父类,需要加上 override
    required override init() {
        // 这里是构造器的实现部分
    }
}

五、协议作为类型

尽管协议本身并未实现任何功能,但是协议可以被当做一个功能完备的类型来使用。协议作为类型使用,有时被称作「存在类型」,这个名词来自「存在着一个类型 T,该类型遵循协议 T」。
协议可以像其他普通类型一样使用,使用场景如下:

  • 作为函数、方法或构造器中的参数类型或返回值类型
  • 作为常量、变量或属性的类型
  • 作为数组、字典或其他容器中的元素类型

注意:
协议是一种类型,因此协议类型的名称应与其他类型(例如 IntDoubleString)的写法相同,使用大写字母开头的驼峰式写法。

下面是将协议作为类型使用的例子:

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
    }
}

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
    }
}

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

六、委托

基本等于我们OC原来常用的代理。

//DiceGame 协议可以被任意涉及骰子的游戏遵循。
protocol DiceGame {
    var dice: Dice { get }
    func play()
}

//iceGameDelegate 协议可以被任意类型遵循,用来追踪 DiceGame 的游戏过程。
protocol DiceGameDelegate {
    func gameDidStart(_ game: DiceGame)
    func game(_ game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
    func gameDidEnd(_ game: DiceGame)
}

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
    }
    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)
    }
}

注意:

为了防止强引用导致的循环引用问题,可以把协议声明为弱引用
public weak var delegate: DiceGameDelegate?

//需要注意代理需要继承class
protocol DiceGameDelegate: class {
    ...
}

如下示例定义了 DiceGameTracker 类,它遵循了 DiceGameDelegate 协议:

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")
    }
}

DiceGameTracker的运行情况如下所示:

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

七、在扩展里添加协议遵循

protocol TextRepresentable {
    var textualDescription: String { get }
}

extension Dice: TextRepresentable {
    var textualDescription: String {
        return "A \(sides)-sided dice"
    }
}

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// 打印 “A 12-sided dice”

注意:
通过扩展令已有类型遵循并符合协议时,该类型的所有实例也会随之获得协议中定义的各项功能。

八、有条件地遵循协议

下面的扩展让 Array 类型只要在存储遵循 TextRepresentable 协议的元素时就遵循 TextRepresentable 协议。

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)
// 打印 "[A 6-sided dice, A 12-sided dice]"

九、在扩展里声明采纳协议

当一个类型已经遵循了某个协议中的所有要求,却还没有声明采纳该协议时,可以通过空的扩展来让它采纳该协议:

struct Hamster {
    var name: String
       var textualDescription: String {
        return "A hamster named \(name)"
    }
}
extension Hamster: TextRepresentable {}

从现在起,Hamster 的实例可以作为 TextRepresentable 类型使用:

let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// 打印 “A hamster named Simon”

注意:
即使满足了协议的所有要求,类型也不会自动遵循协议,必须显式地遵循协议。

十、协议类型的集合

let things: [TextRepresentable] = [game, d12, simonTheHamster]

for thing in things {
    print(thing.textualDescription)
}
// A game of Snakes and Ladders with 25 squares
// A 12-sided dice
// A hamster named Simon

注意 thing 常量是 TextRepresentable 类型而不是 DiceDiceGameHamster 等类型,即使实例在幕后确实是这些类型中的一种。由于 thingTextRepresentable 类型,任何 TextRepresentable 的实例都有一个 textualDescription 属性,所以在每次循环中可以安全地访问 thing.textualDescription

十一、协议的继承

协议能够继承一个或多个其他协议,可以在继承的协议的基础上增加新的要求。协议的继承语法与类的继承相似,多个被继承的协议间用逗号分隔:

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // 这里是协议的定义部分
}

//如下所示,PrettyTextRepresentable 协议继承了 TextRepresentable 协议:
protocol PrettyTextRepresentable: TextRepresentable {
    var prettyTextualDescription: String { get }
}

例子中定义了一个新的协议 PrettyTextRepresentable,它继承自 TextRepresentable 协议。任何遵循 PrettyTextRepresentable 协议的类型在满足该协议的要求时,也必须满足 TextRepresentable 协议的要求。

十二、类专属的协议

你通过添加 AnyObject 关键字到协议的继承列表,就可以限制协议只能被类类型采纳(以及非结构体或者非枚举的类型)。

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    // 这里是类专属协议的定义部分
}

在以上例子中,协议 SomeClassOnlyProtocol 只能被类类型采纳。如果尝试让结构体或枚举类型采纳 SomeClassOnlyProtocol,则会导致编译时错误。

注意:
当协议定义的要求需要遵循协议的类型必须是引用语义而非值语义时,应该采用类类型专属协议。

十三、协议合成

协议组合使用 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)
// 打印 “Happy birthday Malcolm - you're 21!”

十四、检查协议一致性

相似于类型转换中描述的 isas 操作符来检查协议一致性,即是否遵循某协议,并且可以转换到指定的协议类型。检查和转换协议的语法与检查和转换类型是完全一样的:

  • is 用来检查实例是否遵循某个协议,若遵循则返回 true,否则返回 false;
  • as? 返回一个可选值,当实例遵循某个协议时,返回类型为协议类型的可选值,否则返回 nil;
  • as! 将实例强制向下转换到某个协议类型,如果强转失败,将触发运行时错误。

十五、可选的协议要求

协议可以定义可选要求,遵循协议的类型可以选择是否实现这些要求。在协议中使用 optional 关键字作为前缀来定义可选要求。可选要求用在你需要和 Objective-C 打交道的代码中。协议和可选要求都必须带上 @objc 属性。标记 @objc 特性的协议只能被继承自 Objective-C 类的类或者 @objc 类遵循,其他类以及结构体和枚举均不能遵循这种协议。

使用可选要求时(例如,可选的方法或者属性),它们的类型会自动变成可选的。比如,一个类型为 (Int) -> String 的方法会变成 ((Int) -> String)?。需要注意的是整个函数类型是可选的,而不是函数的返回值。

@objc protocol CounterDataSource {
    @objc optional func increment(forCount count: Int) -> Int
    @objc optional var fixedIncrement: Int { get }
}

注意:
严格来讲,CounterDataSource 协议中的方法和属性都是可选的,因此遵循协议的类可以不实现这些要求,尽管技术上允许这样做,不过最好不要这样写。可以通过扩展加默认实现来达到这个效果。

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
        }
    }
}

十六、可选的协议要求

协议可以通过扩展来为遵循协议的类型提供属性、方法以及下标的实现。通过这种方式,你可以基于协议本身来实现这些功能,而无需在每个遵循协议的类型中都重复同样的实现,也无需使用全局函数。

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}

let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// 打印 “Here's a random number: 0.37464991998171”
print("And here's a random Boolean: \(generator.randomBool())")
// 打印 “And here's a random Boolean: true”

协议扩展可以为遵循协议的类型增加实现,但不能声明该协议继承自另一个协议。协议的继承只能在协议声明处进行指定。

十七、提供默认实现

可以通过协议扩展来为协议要求的方法、计算属性提供默认的实现。如果遵循协议的类型为这些要求提供了自己的实现,那么这些自定义实现将会替代扩展中的默认实现被使用。

注意:
通过协议扩展为协议要求提供的默认实现和可选的协议要求不同。虽然在这两种情况下,遵循协议的类型都无需自己实现这些要求,但是通过扩展提供的默认实现可以直接调用,而无需使用可选链式调用。

十八、为协议扩展添加限制条件

扩展协议的时候,可以指定一些限制条件,只有遵循协议的类型满足这些限制条件时,才能获得协议扩展提供的默认实现。这些限制条件写在协议名之后,使用 where 子句来描述,正如 泛型 Where 子句 中所描述的。

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())
// 打印 "true"
print(differentNumbers.allEqual())
// 打印 "false"

注意:
如果一个遵循的类型满足了为同一方法或属性提供实现的多个限制型扩展的要求, Swift 会使用最匹配限制的实现。