天天看點

Swift程式設計二十二(協定)協定

案例代碼下載下傳

協定

協定定義的适應特定任務或功能塊的方法,屬性和其他需求的方案。然後,可以通過類,結構或枚舉來遵守該協定,以提供這些要求的實際實作。任何滿足協定要求的類型都被認為遵守該協定。

除了指定遵守的類型必須實作的要求之外,還可以擴充協定以實作符遵守的類型可以使用的一些需求或其他功能。

協定文法

可以使用與類,結構和枚舉非常類似的方式定義協定:

protocol SomeProtocol {
    // 協定在這裡定義
}
           

聲明遵守特定協定的自定義類型,作為其定義的一部分,将協定名稱放在類型名稱後面并用冒号隔開。可以列出多個協定,并以逗号分隔:

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // 結構體在這裡定義
}
           

如果一個類有超類,則在遵守任何協定之前列出超類名,後跟一個逗号:

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // 類在這裡定義
}
           

協定要求

協定可以要求遵守該協定的類型提供具有特定名稱和類型的執行個體屬性或類型屬性。協定未指定屬性是存儲屬性還是計算屬性 - 它僅指定所需的屬性名稱和類型。協定還指定每個屬性是否必須是可擷取的,可擷取的和可設定的。

如果協定要求屬性可擷取和可設定,則不能通過常量存儲屬性或隻讀計算屬性來滿足該屬性要求。如果協定隻需要一個可擷取屬性,那麼任何類型的屬性都可以滿足要求,如果對你自己的代碼有用可設定的屬性也是有效的。

屬性要求始終聲明為變量屬性,字首為var關鍵字。可擷取和可設定屬性通過在類型聲明後寫入{ get set }來訓示,并且可通過寫入{ get }來訓示可擷取屬性。

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

在協定中定義類型屬性始終要求使用關鍵字static添加字首。即使在類實作時類型屬性要求可以使用class或static關鍵字作為字首,此規則也适用:

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

以下是具有單執行個體屬性要求的協定示例:

protocol FullyNamed {
    var fullName: String { get }
}
           

FullyNamed協定要求遵守的類型以提供完全限定的名稱。該協定沒有指定有關遵守的類型性質的任何其他内容 - 它隻指定該類型必須能夠為自己提供全名。該協定聲明任何遵守FullyNamed協定的類型必須具有一個名為fullName的可擷取執行個體屬性,該屬性屬于String類型。

這是一個遵守FullyNamed協定的簡單結構示例:

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

此示例定義一個名為Person的結構,該結構表示特定的命名人員。聲明它遵守ullyNamed協定作為其定義第一行的一部分。

每個執行個體Person都有一個名為fullName的存儲屬性,屬性類型String。這符合FullyNamed協定的單一要求,并且意味着Person已正确符合協定。(如果未滿足協定要求,Swift會在編譯時報告錯誤。)

這是一個更複雜的類,它也遵守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")
print(ncc1701.fullName)
           

此類将協定要求的fullName屬性實作為飛船的計算隻讀屬性。每個Starship類執行個體都存儲一個強制項name和一個可選項prefix。fullName屬性使用prefix值(如果存在),并将其添加到name開頭,以便為星艦建立全名。

方法要求

協定可能需要通過遵守的類型來實作特定的執行個體方法和類型方法。這些方法作為協定定義的一部分編寫,與普通執行個體和類型方法完全相同,但沒有花括号或方法體。允許使用變量參數,遵循與正常方法相同的規則。但是,無法為協定定義中的方法參數指定預設值。

與類型屬性要求一樣,當在協定中定義類型方法時始終要求使用關鍵字static添加字首。即使在類實作時類型方法要求以classor static關鍵字為字首,也是如此:

protocol SomeProtocol {
    static func someTypeMethod()
}
           

以下示例使用單個執行個體方法的需求定義協定:

protocol RandomNumberGenerator {
    func random() -> Double
}
           

RandomNumberGenerator協定要求任何遵守的類型都有一個名為random的執行個體方法,隻要調用它就會傳回一個Double值。可以假設該值是0.0到1.0(但不包括)的數字,即使這沒有被指定為協定的一部分。

RandomNumberGenerator協定不對如何生成每個随機數做出任何假設 - 它隻需要生成器提供生成新随機數的标準方法。

這是一個遵守RandomNumberGenerator協定的類的實作。此類實作的僞随機數生成器算法稱為線性同餘生成器:

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())")
print("And another one: \(generator.random())")
           

Mutating方法要求

有時需要一種方法來修改(或改變)它所屬的執行個體。例如,關于值類型(即結構和枚舉)将關鍵字mutating放在方法的func關鍵字之前,以訓示允許該方法修改它所屬的執行個體以及該執行個體的任何屬性。在執行個體方法中修改值類型中描述了此過程。

如果定義了一個協定執行個體方法要求改變遵守該協定的任何類型的執行個體,請使用mutating關鍵字作為協定定義的一部分來标記該方法。這使得結構和枚舉能夠采用協定并滿足該方法要求。

注意: 如果将協定執行個體方法要求标記為mutating,則mutating在為類編寫該方法的實作時,不需要編寫關鍵字。該mutating關鍵字僅由結構和枚舉使用。

下面的示例定義了一個名為Togglable的協定,它定義了一個名為toggle的執行個體方法。顧名思義,toggle()方法旨在通過修改該類型的屬性來切換或反轉任何遵守協定類型的狀态。

toggle()方法使用mutating關鍵字作為Togglable協定定義的一部分進行标記,以訓示該方法在調用時會改變執行個體的狀态:

protocol Togglable {
    mutating func toggle()
}
           

如果Togglable協定被結構或枚舉實作,則該結構或枚舉可以通過提供标記mutating的toggle()方法來實作遵守的協定。

下面的示例定義了一個名為OnOffSwitch的枚舉。這個枚舉在on和off兩個狀态之間切換。枚舉的toggle實作标記為mutating,以比對Togglable協定的要求:

enum OnOffSwitch: Togglable {
    case on, off
    mutating func toggle() {
        switch self {
        case .on:
            self = .off
        default:
            self = .on
        }
    }
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
print(lightSwitch)
           

初始化程式要求

協定可能需要通過遵守的類型來實作特定的初始化程式。可以将這些初始化程式作為協定定義的一部分編寫,其方式與普通初始化程式完全相同,但不使用花括号或初始化程式主體:

protocol SomeProtocol {
    init(someParameter: Int)
}
           

協定初始化程式要求的類實作

可以将遵守協定的類初始化程式要求實作為指定初始化程式或便捷初始化程式。在這兩種情況下,都必須使用required修飾符标記初始化程式實作:

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        //在這裡實作初始化
    }
}
           

required修飾符的使用可確定在遵守的類的所有子類上提供顯式或繼承的實作初始化程式的需求,以便能夠遵守協定。

有關必須的初始化的更多資訊,請參閱必需的初始化器。

注意: 不能在使用final修飾符标記的類上使用required修飾符标記的協定初始化程式實作,因為最終類不能進行子類化。有關final修飾符的更多資訊,請參閱防止覆寫。

如果子類重寫超類中的指定初始化程式,并且還實作遵守協定的初始化程式,使用required和override修飾符标記初始化程式實作:

protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        //在這裡實作初始化
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required"來自遵守SomeProtocol協定; "override"來自繼承SomeSuperClass父類
    required override init() {
        //在這裡實作初始化
    }
}
           

Failable初始化程式要求

協定可以定義遵守類型的Failable初始化程式,如Failable Initializers中所定義。

遵守的類型中failable或nonfailable的初始化程式可以滿足failable的初始化程式要求。nonfailable初始化程式或隐式解包的failable初始化程式可以滿足nonfailable初始化程式要求。

作為類型的協定

協定本身并不實作任何功能。盡管如此,建立的任何協定都将成為完全成熟的類型。

因為它是一種類型,是以可以在其他類型允許的許多地方使用協定,包括:

  • 作為函數,方法或初始化程式中的參數類型或傳回類型
  • 作為常量,變量或屬性的類型
  • 作為數組,字典或其他容器中的項類型

注意: 由于協定是類型,他們的名稱以大寫字母(如FullyNamed和RandomNumberGenerator)開始,配合Swift中其他類型的名稱(如Int,String和Double)。

以下是用作類型的協定示例:

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

此示例定義了一個名為Dice的新類,它表示用于棋盤遊戲的n面骰子。Dice執行個體有一個名為sides的整數屬性,它表示它們有多少面,以及一個名為generator的屬性,它提供了一個随機數生成器,用于建立骰子滾動值。

generator屬性是RandomNumberGenerator類型。是以,可以将其設定為遵守RandomNumberGenerator協定的任何類型的執行個體。除了執行個體必須遵守RandomNumberGenerator協定之外,配置設定給此屬性的執行個體不需要任何其他内容。

Dice還有一個初始化器,用于設定其初始狀态。此初始化程式具有一個名為generator的參數,該參數也是RandomNumberGenerator類型。初始化新Dice執行個體時,可以将任何符合類型的值傳遞給此參數。

Dice提供了一個執行個體方法roll,它傳回1到骰子面數之間的整數值。此方法調用生成器的random()方法在0.0和1.0之間建立一個新的随機數,并使用此随機數在正确的範圍内建立骰子滾動值。因為generator已知采用RandomNumberGenerator,是以保證有一種random()方法可以調用。

以下是如何使用Dice類以LinearCongruentialGenerator作為随機數生成器建立六面骰子:

var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
    print("Random dice roll is \(d6.roll())")
}
           

代理

代理是一種設計模式,它使類或結構能夠将其部分職責交給另一種類型(或代理)的執行個體。通過定義封裝代理職責的協定來實作此設計模式,進而保證遵守協定的類型(稱為代理)提供已委派的功能。代理可用于響應特定操作,或從外部源檢索資料,而無需知道該源的基礎類型。

以下示例定義了兩種用于基于骰子的棋盤遊戲的協定:

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協定是任何涉及骰子的遊戲都可以遵守的協定。

可以遵守DiceGameDelegate協定來跟蹤DiceGame的進度。為了防止強引用循環,代理被聲明為弱引用。有關弱引用的資訊,請參閱類執行個體之間的強引用循環。本章後面聲明的SnakesAndLadders類的代理必須使用弱引用,是以将協定标記為class-only協定。class-only協定由繼承AnyObject标記,如“class-only協定”中所述。

這是最初在控制流中引入的蛇梯棋遊戲的一個版本。該版本适用于擲骰子執行個體; 遵守DiceDice協定; 并通知GameDiceGameDelegate其進展情況:

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

有關蛇梯棋遊戲的描述,請參閱Break。

這個版本的遊戲被包裝成一個名為SnakesAndLadders的類,它遵守DiceGame協定。它提供可擷取的dice屬性和play()方法以符合協定。(dice屬性被聲明為常量屬性,因為它在初始化後不需要更改,并且協定隻要求它必須是可擷取的。)

蛇梯棋遊戲闆的設定在類的init()初始化内進行。所有遊戲邏輯都被移入協定的play方法,該方法使用協定的必需dice屬性來提供其骰子滾動值。

請注意,delegate屬性被定義為DiceGameDelegate可選值,因為玩遊戲代理不是必須的。因為它是可選類型,是以該delegate屬性會自動設定為初始值nil。此後,遊戲執行個體化器可以選擇将屬性設定為合适的代理。由于DiceGameDelegate協定僅支援類,是以可以聲明代理為weak以防止引用循環。

DiceGameDelegate提供了三種跟蹤遊戲進度的方法。這三種方法已經被合并到上述play()方法中的遊戲邏輯中,并且在新遊戲開始,新轉彎開始或遊戲結束時被調用。

由于delegate屬性是可選的DiceGameDelegate,是以每次調用代理上的play()方法時,該方法都使用可選連結。如果delegate屬性為nil,則這些委托調用會正常失敗并且沒有錯誤。如果delegate屬性不為nil,則調用代理方法,并将SnakesAndLadders執行個體作為參數傳遞。

下一個示例顯示了一個名為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實作DiceGameDelegate協定所需的所有三種方法。它使用這些方法來跟蹤遊戲的擲骰次數。numberOfTurns在遊戲開始時将屬性重置為零,每次新擲骰時将其增加,并在遊戲結束後列印出總擲骰數。

上面所示的gameDidStart(:)實作使用該game參數來列印關于即将開始的遊戲的一些介紹性資訊。該game參數的類型為DiceGame,而不是SnakesAndLadders,是以gameDidStart(:)隻能通路和使用作為DiceGame協定一部分實作的方法和屬性。但是,該方法仍然可以使用類型轉換來查詢基礎執行個體的類型。在此示例中,它檢查是否game實際上是SnakesAndLadders執行個體,如果是,則列印相應的消息。

gameDidStart(:)方法還通路傳遞的game參數的dice屬性。因為game已知它符合DiceGame協定,是以它保證具有dice屬性,是以該gameDidStart(:)方法能夠通路和列印骰子的sides屬性,無論正在進行的什麼類型的遊戲。

這是DiceGameTracker看起來如何進行:

let tracker = DiceGameTracker()
let game = SnakesAndLadders()
game.delegate = tracker
game.play()
           

擴充中添加協定

即使無權通路現有類型的源代碼,也可以擴充現有類型以采用并符合新協定。擴充可以向現有類型添加新屬性,方法和下标,是以可以添加協定可能要求的任何要求。有關擴充的更多資訊,請參閱擴充。

注意: 當在擴充中的添加協定時,類型的現有執行個體會自動遵守協定。

例如,這個被稱為TextRepresentable的協定可以通過任何可以表示為文本的方式實作。這可能是對自身的描述,也可能是其目前狀态的文本版本:

protocol TextRepresentable {
    var textualDescription: String { get }
}
           

上述Dice類可以擴充為遵守TextRepresentable協定:

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

此擴充遵守新協定的方式與Dice在原始實作中提供的方式完全相同。協定名稱在類型名稱後面提供,用冒号分隔,并且在擴充的花括号内提供協定的所有要求的實作。

Dice現在可以将任何執行個體視為TextRepresentable:

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
           

同樣,SnakesAndLadders遊戲類可以擴充為遵守TextRepresentable協定:

extension SnakesAndLadders: TextRepresentable {
    var textualDescription: String {
        return "A game of Snakes and Ladders with \(finalSquare) squares"
    }
}
print(game.textualDescription)
           

有條件地遵守協定

泛型類型可能僅在某些條件下滿足協定的要求,例如當泛型參數遵守協定時。在擴充類型時列出限制可以使泛型類型有條件地符合協定。通過編寫泛型where子句,在遵守的協定名稱之後寫下這些限制。有關泛型where子句的更多資訊,請參閱泛型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)
           

通過擴充聲明遵守協定

如果某個類型已經符合協定的所有要求,但尚未聲明它遵守該協定,則可用遵守協定的空擴充:

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)
           

注意: 類型不會僅通過滿足其要求自動遵守協定。必須始終明确聲明他們遵守協定。

協定類型的集合

協定可以用作要存儲在諸如數組或字典之類的集合中的類型,如作為類型的協定中所述。這個例子建立了一個TextRepresentable數組:

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

在可以疊代數組中的項,并列印每個項目的文本描述:

for thing in things {
    print(thing.textualDescription)
}
           

請注意,thing常量是TextRepresentable類型。它不是Dice類型或者DiceGame、Hamster即使幕後的實際執行個體屬于這些類型之一。盡管如此,因為它是TextRepresentable類型,并且任何TextRepresentable已知具有textualDescription屬性,是以thing.textualDescription每次通過循環通路是安全的。

協定繼承

協定可以繼承一個或多個其他協定,并可以在其繼承的需求之上添加進一步的要求。協定繼承的文法類似于類繼承的文法,但是可以選擇列出多個繼承的協定,用逗号分隔:

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // 在這裡定義協定
}
           

以下是繼承上述TextRepresentable協定的協定示例:

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

此示例定義了一個繼承自TextRepresentable的新協定PrettyTextRepresentable。遵守PrettyTextRepresentable的任何對象必須滿足TextRepresentable的所有要求,以及PrettyTextRepresentable的要求。在這個例子中,PrettyTextRepresentable将提供一個稱為prettyTextualDescription的gettable屬性傳回一個String。

SnakesAndLadders類可擴充到遵守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
    }
}
           

聲明此擴充遵守PrettyTextRepresentable協定并提供SnakesAndLadders類型的prettyTextualDescription屬性的實作。任何遵守PrettyTextRepresentable的對象必須遵守TextRepresentable是以prettyTextualDescription通過從TextRepresentable協定通路textualDescription屬性開始實作輸出字元串。并附加上冒号和換行符作為prettyTextualDescription的開頭。然後通過疊代棋盤方塊的數組,并附加幾何形狀來表示每個方格的内容:

  • 如果方格的值大于0,則它是梯子的底部,由▲表示。
  • 如果方格的值小于0,則它是蛇的頭部,由▼表示。
  • 否則方格的值是0,它是一個“自由”方格,由○表示。

prettyTextualDescription屬性現在可用于列印任何SnakesAndLadders執行個體的文本描述:

print(game.prettyTextualDescription)
           

Class-Only協定

可以通過将AnyObject協定添加到協定的繼承清單來将協定采用限制為類類型(而不是結構或枚舉)。

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    //在這裡定義class-only協定
}
           

在上面的示例中,SomeClassOnlyProtocol隻能由類類型遵守。編寫試圖遵守SomeClassOnlyProtocol的結構或枚舉定義編譯時報錯誤。

注意: 當該協定的要求定義的行為假定或要求符合類型具有引用語義而不是值語義時,請使用Class-Only協定。有關引用和值語義的更多資訊,請參閱結構和枚舉是值類型和類是引用類型。

協定組合

要求類型同時遵守多個協定可能很有用。可以使用協定組合将多個協定組合到單個需求中。協定組合的行為就像定義了一個臨時本地協定,該協定能夠組合所有協定。協定組合不定義任何新的協定類型。

協定組合具有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)
           

在此示例中,Named協定要求有一個名為mame類型為String的gettable屬性。Aged協定要求有一個名為age類型為Int的gettable屬性。兩種協定都被Person結構遵守。

示例還定義了一個wishHappyBirthday(to:)函數。celebrator參數的類型是Named & Aged,“任何遵守Named和Aged協定的類型。”隻要符合兩個必需的協定,哪個特定類型傳遞給函數都無關緊要。

然後,示例建立一個名為birthdayPerson的Person新執行個體,并将此新執行個體傳遞給該wishHappyBirthday(to:)函數。因為Person符合這兩種協定,是以此調用有效,并且wishHappyBirthday(to:)函數可以列印其生日問候語。

這是一個将前一個示例中的Named協定與Location類組合在一起的示例:

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)
           

beginConcert(in:)函數采用Location & Named類型的參數,在這種情況下意味着滿足“任何Location類型的子類,并且遵守Named協定。”這兩個要求。City

傳遞birthdayPerson給beginConcert(in:)函數是無效的,因為Person它不是Location子類。同樣,如果您建立了一個Location不符合Named協定的子類,則beginConcert(in:)使用該類型的執行個體調用也是無效的。

檢查協定是否一緻

可以使用類型轉換中描述的is和as運算符來檢查協定是否一緻,以及轉換為特定協定。檢查并轉換為協定遵循與檢查和轉換為類型完全相同的文法:

  • is運算符如果一個執行個體遵守協定傳回true,如果它不遵守傳回false,。
  • as?運算符的版本傳回協定類型的可選值,如果執行個體不符合該協定則此值為nil。
  • as!如果向下轉換不成功,則向下轉換運算符的版本強制向下轉換為協定類型并觸發運作時錯誤。

此示例定義了一個名為HasArea的協定,其中要求包含單個Double類型的gettable屬性的屬性area:

protocol HasArea {
    var area: Double { get }
}
           

這裡有兩個類,Circle和Country,這兩者都遵守HasArea協定:

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

要求Circle類實作的屬性area是基于存儲radius的計算屬性。要求Country類實作的area則直接是存儲屬性。兩個類都正确地遵守HasArea協定。

這是一個名為Animal的類,它不遵守HasArea協定:

class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs }
}
           

Circle、Country和Animal類沒有共享的基類。盡管如此,但它們都是類,是以所有三種類型的執行個體都可用于初始化存儲AnyObject類型值的數組:

let objects: [AnyObject] = [
    Circle(radius: 2.0),
    Country(area: 243_610),
    Animal(legs: 4)
]
           

使用半徑為2.0的Circle執行個體、以英裡平方公裡的面積初始化的Country執行個體還有四條腿的Animal執行個體的數組文字初始化objects數組;。

現在可以疊代objects數組,并且可以檢查數組中的每個對象以檢視它是否遵守HasArea協定:

for object in objects {
    if let objectWithArea = object as? HasArea {
        print("Area is \(objectWithArea.area)")
    } else {
        print("Something that doesn't have an area")
    }
}
           

隻要數組中的對象符合HasArea協定,as?操作符傳回的可選值就會被解包,并且可選綁定到一個叫做objectWithArea的常量中。objectWithArea常量已知是HasArea類型,是以能夠類型安全的通路并列印area屬性。

請注意,建構過程不會更改基礎對象。他們仍然是Circle,Country和Animal。然而,在它們存儲在objectWithArea常量中時,它們隻是已知的HasArea類型,是以隻能通路它們的area屬性。

可選要求的協定

可以定義可選要求的協定,遵守協定的類型不一定要實作這些要求。可選要求以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 }
}
           

CounterDataSource協定要求定義一個名為increment(forCount:)的可選方法和一個名為fixedIncrement的可選屬性。這些要求為資料源Counter執行個體定義兩種不同方式來提供适當增量。

注意: 嚴格來說,可以編寫遵守CounterDataSource而無需實作任何協定要求的自定義類。畢竟,它們都是可選的。雖然技術上允許,但這不是非常好的資料源。

下面定義的Counter類具有CounterDataSource?類型的可選屬性dataSource:

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

Counter類的目前值存儲在一個名為count變量屬性中。Counter類也定義了一個稱為increment的方法,每次方法調用時遞增count屬性。

increment()方法首先嘗試通過increment(forCount:)在其資料源上查找該方法的實作來檢索增量。increment()方法使用可選連結來嘗試調用increment(forCount:),每次調用這個方法來增加count屬性。

請注意,此處有兩個級别的可選連結。首先,dataSource可能是nil,是以dataSource在其名稱後面有一個問号,表示隻有在dataSource不是nil時才應該調用increment(forCount:)。其次,即使dataSource不為nil,也不能保證它實作了increment(forCount:),因為它是一個可選要求。在這裡,increment(forCount:)可能未實作的可能性也由可選連結處理。調用increment(forCount:)僅在increment(forCount:)存在時發生-即不為nil。這就是為什麼increment(forCount:)在其名稱後面還帶有問号。

由于這兩個原因之一increment(forCount:)的調用可能會失敗,是以調用傳回一個可選 Int值。即使CounterDataSource在increment(forCount:)定義中傳回非可選Int值也是如此。即使有兩個可選的連結操作,一個接一個,結果仍然是一個可選值。有關使用多個可選連結操作的詳細資訊,請參閱連結多個可選鍊。

在調用之後increment(forCount:)傳回的Int可選項使用可選綁定被解包為名為的amount常量。如果可選Int包含一個值 - 即,如果代理和方法都存在,并且該方法傳回一個值 - 則将存儲屬性count解包添加到amount中并完成增量。

如果無法從increment(forCount:)方法中檢索值- 因為dataSource是nil,或者因為資料源沒有實作increment(forCount:)- 那麼該increment()方法會嘗試從資料源的fixedIncrement屬性中檢索值。該fixedIncrement屬性也是一個可選要求,是以它的值是一個可選Int值,即使作為CounterDataSource協定定義的一部分fixedIncrement被定義為非可選Int屬性。

這是一個簡單的CounterDataSource實作,其中資料源每次查詢時傳回常量值3。它通過實作可選fixedIncrement屬性要求來實作:

class ThreeSource: NSObject, CounterDataSource {
    let fixedIncrement = 3
}
           

可以使用ThreeSource執行個體作為新Counter執行個體的資料源:

var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
    counter.increment()
    print(counter.count)
}
           

上面的代碼建立了一個新Counter執行個體; 将其資料源設定為新ThreeSource執行個體; 并且四次調用計數器的方法increment()。正如預期的那樣,每次調用increment()計數器方法count屬性增加3 。

這是一個更複雜的資料源TowardsZeroSource,它使Counter執行個體從其目前count值向上或向下計數到零:

class TowardsZeroSource: NSObject, CounterDataSource {
    func increment(forCount count: Int) -> Int {
        if count == 0 {
            return 0
        } else if count < 0 {
            return 1
        } else {
            return -1
        }
    }
}
           

TowardsZeroSource類從CounterDataSource協定實作可選的increment(forCount:)方法使用count參數值來計算出計數的方向。如果count已經是零,則該方法傳回0表示沒有進一步的計數應該發生。

現有可以使用TowardsZeroSourceCounter執行個體的執行個體從-4開始計數。一旦計數器達到零,就不再進行計數:

counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
    counter.increment()
    print(counter.count)
}
           

協定擴充

可以擴充協定以向遵守的類型提供方法,初始化器,下标和計算屬性實作。這允許定義協定本身的行為,而不是每種類型的單獨一緻性或全局函數。

例如,RandomNumberGenerator可以擴充協定以提供一種randomBool()方法,該方法使用所需random()方法的結果來傳回随機Bool值:

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

通過在協定上建立擴充,所有符合類型的類型自動獲得此方法實作,而無需任何其他修改。

let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
print("And here's a random Boolean: \(generator.randomBool())")
           

協定擴充可以為遵守的類型添加實作,但不能使協定擴充繼承自其他協定。協定繼承總是在協定聲明本身中指定。

提供預設實作

可以使用協定擴充為協定要求的任何方法或計算屬性提供預設實作。如果遵守的類型提供其自己的必需方法或屬性的實作,則将使用該實作而不是擴充提供的實作。

注意: 擴充提供的協定要求的預設實作與可選協定不同。雖然遵守的類型可以不提供它們自己的實作,但是可以在沒有可選連結的情況下調用具有預設實作的需求。

例如,繼承TextRepresentable協定的PrettyTextRepresentable協定可以提供其必需的prettyTextualDescription屬性的預設實作,以簡單地傳回通路textualDescription屬性的結果:

extension PrettyTextRepresentable {
    var prettyTextualDescription: String {
        return textualDescription
    }
}
           

添加限制到協定擴充

定義協定擴充時,可以在擴充的方法和屬性可用之前指定符合類型必須滿足的限制。可以通過在擴充的協定名稱之後編寫generic where子句來編寫這些限制。有關generic where子句的更多資訊,請參閱Generic Where子句。

例如,可以定義Collection協定的擴充,該擴充适用于其元素遵守Equatable協定的任何集合。通過将集合的元素限制到Equatable協定(标準庫的一部分),可以使用==和!=運算符來檢查兩個元素之間的相等性和不相等。

extension Collection where Element: Equatable {
    func allEqua() -> Bool {
        for element in self {
            if element != self.first {
                return false
            }
        }
        return true
    }
}
           

僅當集合中的所有元素相等時,allEqual()方法才傳回true。

考慮兩個整數數組,一個是所有元素都相同,另一個不是:

let equalNumbers = [100, 100, 100, 100, 100]
let differentNumbers = [100, 100, 200, 100, 200]
           

因為數組遵守Collection并且Int遵守Equatable,equalNumbers和differentNumbers可以使用allEqual()方法:

print(equalNumbers.allEqual())
print(differentNumbers.allEqual())
           

注意: 如果遵守的類型滿足多個限制擴充要求對同一方法或屬性提供實作,則Swift使用與最專用限制相對應的實作。