Swift與泛型編程第一彈:基礎語法之協議

        泛型編程是一種很古老的技術,早在上世紀90年代就有大規模的應用,特別是在C++語言中得到了廣泛的應用。C++的STL從大的方面來看包括:數據結構、算法、迭代器,這些內容都是基於泛型的,可以說泛型基礎支撐了STL大廈。如果諸位讀者對C++泛型編程感興趣,想要深入瞭解的話,推薦兩本書《STL源碼剖析》、《C++Template》。
    Swift同樣支持泛型編程(雖然與C++的泛型相比依然弱很多)。Swift的許多基礎設施如Array、Dictionary等都是基於泛型技術來實現的。在日常工作中,如果編寫一些供其他同事使用的基礎組件時,一般會有較多的場景使用到泛型技術。
    本系列文章就是從基礎支持語法開始,逐步探索在Swift如何使用泛型編程。

注意
本文是基於官方的《Swift編程語言》的協議章節,在作者重溫相關內容之後,把一些重要內容複製過來的,所以原創程度不高。從第三彈開始,後面章節的原創程度會逐步提高。

1.1 協議

  • 定義了一個藍圖,規定了用來實現某一特定任務或者功能的方法、屬性,以及其他需要的東西
  • 類、結構體或枚舉都可以遵循協議
  • 可以要求遵循協議的類型提供特定名稱和類型的實例屬性或類型屬性
// 協議定義
protocol SomeProtocol {
    // 這裏是協議的定義部分
}

// 遵循多個協議時,各協議之間用逗號(,)分隔
struct SomeStructure: FirstProtocol, AnotherProtocol {
    // 這裏是結構體的定義部分
}

// 若是一個類擁有父類,應該將父類名放在遵循的協議名之前,以逗號分隔
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol {
    // 這裏是類的定義部分
}

1.1.1 屬性

  • 協議不指定屬性是存儲屬性還是計算屬性,它只指定屬性的名稱和類型
  • 協議還指定屬性是可讀的還是可讀可寫的
  • 如果協議要求屬性是可讀可寫的,那麼該屬性不能是常量屬性或只讀的計算型屬性。
  • 如果協議只要求屬性是可讀的,那麼該屬性不僅可以是可讀的,如果代碼需要的話,還可以是可寫的。
// 協議總是用 var 關鍵字來聲明變量屬性
// 在類型聲明後加上 { set get } 來表示屬性是可讀可寫的,可讀屬性則用 { get } 來表示:
protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}

// 在協議中定義類型屬性時,總是使用 static 關鍵字作爲前綴。當類類型遵循協議時,除了 static 關鍵字,還可以使用 class 關鍵字來聲明類型屬性:
protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
}

// 如下所示,這是一個只含有一個實例屬性要求的協議:
protocol FullyNamed {
    var fullName: String { get }
}

// 下面是一個遵循 FullyNamed 協議的簡單結構體:
struct Person: FullyNamed {
    var fullName: String
}
let john = Person(fullName: "John Appleseed")
// john.fullName 爲 "John Appleseed"

// 下面是一個更爲複雜的類,它採納並遵循了 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 爲 "USS Enterprise"

1.1.2 方法

  • 方法作爲協議的一部分,像普通方法一樣放在協議的定義中,但是不需要大括號和方法體
  • 可以在協議中定義具有可變參數的方法,和普通方法的定義方式相同
  • 但是,不支持爲協議中的方法提供默認參數
  • 定義類型方法時,可以使用static
protocol SomeProtocol {
    static func someTypeMethod()
}

protocol RandomNumberGenerator {
    func random() -> Double
}

// 性同餘生成器(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).truncatingRemainder(dividingBy: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”

// 如果你在協議中定義了一個實例方法,該方法會改變遵循該協議的類型的實例,那麼在定義協議時需要在方法前加 mutating 關鍵字。
// 這使得結構體和枚舉能夠遵循此協議並滿足此方法要求。

// toggle() 方法在定義的時候,使用 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

1.1.3 構造器要求

  • 協議可以要求遵循協議的類型實現指定的構造器
  • 你可以像編寫普通構造器那樣,在協議的定義裏寫下構造器的聲明,但不需要寫花括號和構造器的實體:
protocol SomeProtocol {
    init(someParameter: Int)
}

// 你可以在遵循協議的類中實現構造器,無論是作爲指定構造器,還是作爲便利構造器
// 無論哪種情況,你都必須爲構造器實現標上 required 修飾符:
class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // 這裏是構造器的實現部分
    }
}

// 如果一個子類重寫了父類的指定構造器,並且該構造器滿足了某個協議的要求,那麼該構造器的實現需要同時標註 required 和 override 修飾符:
protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        // 這裏是構造器的實現部分
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // 因爲遵循協議,需要加上 required
    // 因爲繼承自父類,需要加上 override
    required override init() {
        // 這裏是構造器的實現部分
    }
}

// 遵循協議的類型可以通過可失敗構造器(init?)或非可失敗構造器(init)來滿足協議中定義的可失敗構造器要求。
// 協議中定義的非可失敗構造器要求可以通過非可失敗構造器(init)或隱式解包可失敗構造器(init!)來滿足。

1.1.4 協議作爲類型

儘管協議本身並未實現任何功能,但是協議可以被當做一個功能完備的類型來使用。協議作爲類型使用,有時被稱作「存在類型」,這個名詞來自「存在着一個類型 T,該類型遵循協議 T」。
協議可以像其他普通類型一樣使用,使用場景如下:

  • 作爲函數、方法或構造器中的參數類型或返回值類型
  • 作爲常量、變量或屬性的類型
  • 作爲數組、字典或其他容器中的元素類型
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

1.1.5 委託

委託是一種設計模式,它允許類或結構體將一些需要它們負責的功能委託給其他類型的實例。委託模式的實現很簡單:定義協議來封裝那些需要被委託的功能,這樣就能確保遵循協議的類型能提供這些功能。委託模式可以用來響應特定的動作,或者接收外部數據源提供的數據,而無需關心外部數據源的類型。
若要聲明類專屬的協議就必須繼承於 AnyObject。

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

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

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

1.1.6 在擴展裏添加協議遵循

  • 即便無法修改源代碼,依然可以通過擴展令已有類型遵循並符合協議。
  • 擴展可以爲已有類型添加屬性、方法、下標以及構造器,因此可以符合協議中的相應要求
  • 通過擴展令已有類型遵循並符合協議時,該類型的所有實例也會隨之獲得協議中定義的各項功能
  • 通過擴展遵循並採納協議,和在原始定義中遵循並符合協議的效果完全相同
// 一個協議
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”

1.1.7 有條件地遵循協議

泛型類型可能只在某些情況下滿足一個協議的要求,比如當類型的泛型形式參數遵循對應協議時。你可以通過在擴展類型時列出限制讓泛型類型有條件地遵循某協議。在你採納協議的名字後面寫泛型 where 分句。
下面的擴展讓 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]"
``
### 1.1.8 在擴展裏聲明採納協議
```swift
// 當一個類型已經遵循了某個協議中的所有要求,卻還沒有聲明採納該協議時,可以通過空的擴展來讓它採納該協議:
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”

// 即使滿足了協議的所有要求,類型也不會自動遵循協議,必須顯式地遵循協議。

1.1.9 使用合成實現來採納協議

Swift 可以自動提供一些簡單場景下遵循 Equatable、Hashable 和 Comparable 協議的實現。在使用這些合成實現之後,無需再編寫重複的代碼來實現這些協議所要求的方法。
Swift 爲以下幾種自定義類型提供了 Equatable(==、!=) 協議的合成實現:

  • 遵循 Equatable 協議且只有存儲屬性的結構體。
  • 遵循 Equatable 協議且只有關聯類型的枚舉
  • 沒有任何關聯類型的枚舉

Swift 爲以下幾種自定義類型提供了 Hashable 協議(hash(into:))的合成實現:

  • 遵循 Hashable 協議且只有存儲屬性的結構體。
  • 遵循 Hashable 協議且只有關聯類型的枚舉
  • 沒有任何關聯類型的枚舉

Swift 爲沒有原始值的枚舉類型提供了 Comparable 協議的合成實現。如果枚舉類型包含關聯類型,那這些關聯類型也必須同時遵循 Comparable 協議。在包含原始枚舉類型聲明的文件中聲明其對 Comparable 協議的遵循,可以得到 < 操作符的合成實現,且無需自己編寫任何關於 < 的實現代碼。Comparable 協議同時包含 <=、> 和 >= 操作符的默認實現。

1.1.10 協議類型的集合

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

1.1.11 協議的繼承

協議能夠繼承一個或多個其他協議,可以在繼承的協議的基礎上增加新的要求。協議的繼承語法與類的繼承相似,多個被繼承的協議間用逗號分隔:

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // 這裏是協議的定義部分
}

protocol PrettyTextRepresentable: TextRepresentable {
    var prettyTextualDescription: String { get }
}

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

1.1.12 類專屬的協議

你通過添加 AnyObject 關鍵字到協議的繼承列表,就可以限制協議只能被類類型採納(以及非結構體或者非枚舉的類型)。

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    // 這裏是類專屬協議的定義部分
}

1.1.13 協議的合成

要求一個類型同時遵循多個協議是很有用的。你可以使用協議組合來複合多個協議到一個要求裏。協議組合行爲就和你定義的臨時局部協議一樣擁有構成中所有協議的需求。協議組合不定義任何新的協議類型。
協議組合使用 SomeProtocol & AnotherProtocol 的形式。你可以列舉任意數量的協議,用和符號(&)分開。除了協議列表,協議組合也能包含類類型,這允許你標明一個需要的父類。
下面的例子中,將 Named 和 Aged 兩個協議按照上述語法組合成一個協議,作爲函數參數的類型:

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!”

class Location {
    var latitude: Double
    var longitude: Double
    init(latitude: Double, longitude: Double) {
        self.latitude = latitude
        self.longitude = longitude
    }
}
class City: Location, Named {
    var name: String
    init(name: String, latitude: Double, longitude: Double) {
        self.name = name
        super.init(latitude: latitude, longitude: longitude)
    }
}
func beginConcert(in location: Location & Named) {
    print("Hello, \(location.name)!")
}

let seattle = City(name: "Seattle", latitude: 47.6, longitude: -122.3)
beginConcert(in: seattle)
// 打印 "Hello, Seattle!"

1.1.14 檢查協議一致性

你可以使用 類型轉換 中描述的 isas 操作符來檢查協議一致性,即是否遵循某協議,並且可以轉換到指定的協議類型。檢查和轉換協議的語法與檢查和轉換類型是完全一樣的:

  • is 用來檢查實例是否遵循某個協議,若遵循則返回 true,否則返回 false
  • as? 返回一個可選值,當實例遵循某個協議時,返回類型爲協議類型的可選值,否則返回 nil
  • as! 將實例強制向下轉換到某個協議類型,如果強轉失敗,將觸發運行時錯誤。
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")
    }
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area

1.1.15 可選的協議要求

協議可以定義可選要求,遵循協議的類型可以選擇是否實現這些要求。在協議中使用 optional 關鍵字作爲前綴來定義可選要求。可選要求用在你需要和 Objective-C 打交道的代碼中。協議和可選要求都必須帶上 @objc 屬性。標記 @objc 特性的協議只能被繼承自 Objective-C 類的類或者 @objc 類遵循,其他類以及結構體和枚舉均不能遵循這種協議。

使用可選要求時(例如,可選的方法或者屬性),它們的類型會自動變成可選的。比如,一個類型爲 (Int) -> String 的方法會變成 ((Int) -> String)?。需要注意的是整個函數類型是可選的,而不是函數的返回值。

協議中的可選要求可通過可選鏈式調用來使用,因爲遵循協議的類型可能沒有實現這些可選要求。類似 someOptionalMethod?(someArgument) 這樣,你可以在可選方法名稱後加上 ? 來調用可選方法。詳細內容可在 可選鏈式調用 章節中查看。

下面的例子定義了一個名爲 Counter 的用於整數計數的類,它使用外部的數據源來提供每次的增量。數據源由 CounterDataSource 協議定義,它包含兩個可選要求:

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

1.1.16 協議擴展

協議可以通過擴展來爲遵循協議的類型提供屬性、方法以及下標的實現。通過這種方式,你可以基於協議本身來實現這些功能,而無需在每個遵循協議的類型中都重複同樣的實現,也無需使用全局函數。

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
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章