阅读 2350

聊一聊Swift协议

协议

协议定义了适合特定任务或功能块的方法、属性和其他需求的蓝图。然后,类、结构或枚举可以采用该协议来提供这些需求的实际实现。任何满足协议要求的类型都被称为符合该协议。

除了指定符合类型必须实现的需求之外,您还可以扩展协议来实现其中的一些需求,或者实现符合类型可以利用的其他功能。

协议的语法

定义协议的方式与类、结构和枚举非常类似:

protocol SomeProtocol {
    // protocol definition goes here
}
复制代码

自定义类型声明它们采用特定的协议,方法是将协议的名称放在类型名称之后,用冒号分隔,作为定义的一部分。可以列出多个协议,并用逗号分隔:

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // structure definition goes here
}
复制代码

如果一个类有一个父类,在它所采用的任何协议之前列出父类的名称,后跟逗号:

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class definition goes here
}
复制代码

属性要求

协议可以要求任何符合规范的类型提供实例属性或具有特定名称和类型的类型属性。协议没有指定该属性是存储属性还是计算属性——它只指定了所需的属性名称和类型。协议还指定了每个属性必须是gettable还是gettable and settable。

如果协议要求属性是gettable和settable,则不能通过常量存储属性或只读计算属性来满足该属性要求。如果协议只要求一个属性是gettable,那么任何类型的属性都可以满足这个要求,如果这个属性对您自己的代码有用,那么它也是settable。

属性要求总是声明为变量属性,前面加上var关键字。Gettable和settable属性在类型声明之后通过写入{get set}表示,Gettable属性通过写入{get}表示。

protocol SomeProtocol {
    var mustBeSettable: Int { get set }
    var doesNotNeedToBeSettable: Int { get }
}
复制代码

在协议中定义static关键字时,请始终使用它们作为类型属性需求的前缀。即使类型属性要求可以在类实现时以class或static关键字作为前缀,该规则仍然适用:

protocol AnotherProtocol {
    static var someTypeProperty: Int { get set }
}
复制代码

下面是一个协议的例子,它只需要一个实例属性:

protocol FullyNamed {
    var fullName: String { get }
}
复制代码

FullyNamed协议需要符合要求的类型来提供完全限定的名称。协议没有指定关于符合类型的任何其他性质—它只指定类型必须能够为自己提供一个全名。协议规定,任何FullyNamed类型都必须具有一个名为fullName的可获取实例属性,该属性的类型为String。

下面是一个简单结构的例子,它采用并符合完全命名的协议:

struct Person: FullyNamed {
    var fullName: String
}
let Simon = Person(fullName: "Simon Yu")
// Simon.fullName is "Simon Yu"
复制代码

本例定义了一个名为Person的结构,它表示一个特定的已命名Person。它声明它采用完全命名的协议作为其定义的第一行的一部分。

Person的每个实例都有一个名为fullName的存储属性,其类型为String。这符合完整命名的协议的单个要求,并且意味着该人员已经正确地符合该协议。(如果没有满足协议要求,Swift会在编译时报告错误。)

下面是一个更复杂的类,它也采用并符合完全命名的协议:

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 is "USS Enterprise"
复制代码

该类将fullName属性要求实现为starship的计算只读属性。每个Starship类实例都存储一个强制名称和一个可选前缀。fullName属性如果存在,则使用前缀值,并将其前置到名称的开头,以创建starship的全名。

方法要求

协议可以要求特定的实例方法和类型方法通过符合类型来实现。这些方法作为协议定义的一部分编写,其方式与普通实例和类型方法完全相同,但是没有花括号或方法体。可变参数是允许的,受相同的规则为正常的方法。但是,不能为协议定义中的方法参数指定默认值。

与类型属性需求一样,当在协议中定义类型方法需求时,总是在它们前面加上static关键字。这是真实的,即使类型方法需求在类实现时以class或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())")
// Prints "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// Prints "And another one: 0.729023776863283"
复制代码

改变方法要求

方法有时需要修改(或修改)它所属的实例。对于值类型(即结构和枚举)上的实例方法,您将mutating关键字放在方法的func关键字之前,以指示允许该方法修改其所属的实例和该实例的任何属性。在从实例方法中修改值类型时描述了这个过程。

如果您定义了一个协议实例方法需求,该需求旨在对采用该协议的任何类型的实例进行更改,那么在该方法上使用mutating关键字作为协议定义的一部分。这使得结构和枚举能够采用协议并满足方法需求。

请注意

如果将协议实例方法需求标记为mutating,则在为类编写该方法的实现时不需要编写mutating关键字。mutating关键字仅用于结构和枚举。
复制代码

下面的示例定义了一个名为Togglable的协议,它定义了一个名为toggle的实例方法需求。顾名思义,toggle()方法旨在切换或反转任何符合类型的状态,通常是通过修改该类型的属性。

toggle()方法作为Togglable协议定义的一部分,使用mutating关键字标记为mutating()方法,表示该方法在被调用时,期望对符合约定的实例的状态进行改变:

protocol Togglable {
    mutating func toggle()
}
复制代码

如果为结构或枚举实现Togglable协议,则该结构或枚举可以通过提供toggle()方法的实现来符合协议,该方法也被标记为mutating。

下面的示例定义了一个名为OnOffSwitch的枚举。此枚举切换在两个状态之间切换,由枚举用例on和off表示。枚举的切换实现标记为mutating,以匹配可切换协议的要求:

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 is now equal to .on
复制代码

初始化要求

协议可以要求特定的初始化器由符合的类型实现。你编写这些初始化器作为协议定义的一部分,与普通初始化器的编写方法完全相同,但是没有花括号或初始化器主体:

rotocol SomeProtocol {
    init(someParameter: Int)
}
复制代码

协议初始化器需求的类实现

您可以在符合规范的类上实现协议初始化器需求,作为指定的初始化器或方便的初始化器。在这两种情况下,都必须用所需的修饰符标记初始化器实现:

class SomeClass: SomeProtocol {
    required init(someParameter: Int) {
        // initializer implementation goes here
    }
}
复制代码

使用required修饰符可以确保在符合规范的类的所有子类上提供显式或继承的初始化器需求实现,以便它们也符合协议。

有关所需初始化程序的更多信息,请参见所需初始化程序。

请注意

您不需要在使用final修饰符标记的类上使用所需的修饰符标记协议初始化器实现,因为final类不能子类化。有关最终修饰符的更多信息,请参见防止覆盖。
复制代码

如果子类从超类重写指定的初始化器,并且还从协议实现匹配的初始化器需求,则用required和override修饰符标记初始化器实现:

protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        // initializer implementation goes here
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // "required" from SomeProtocol conformance; "override" from SomeSuperClass
    required override init() {
        // initializer implementation goes here
    }
}
复制代码

Failable初始化需求

协议可以为符合类型定义可失败初始化器需求,就像可失败初始化器中定义的那样。

可失败初始化器需求可以由符合类型上的可失败初始化器或不可失败初始化器来满足。不可失败初始化器需求可以由不可失败初始化器或隐式展开的不可失败初始化器来满足。

协议的类型

协议本身实际上并不实现任何功能。尽管如此,您创建的任何协议都将成为代码中使用的成熟类型。

因为它是一种类型,所以您可以在许多允许使用其他类型的地方使用协议,包括:

  • 作为函数、方法或初始化器中的参数类型或返回类型

  • 作为常量、变量或属性的类型

  • 作为数组、字典或其他容器中的Item的类型

请注意

因为协议是类型,所以它们的名称以大写字母开头(如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的属性,它提供了一个随机数生成器,从中可以创建Dice滚动后的值。

生成器属性属于RandomNumberGenerator类型。因此,您可以将其设置为采用RandomNumberGenerator协议的任何类型的实例。除了实例必须采用RandomNumberGenerator协议外,不需要对分配给此属性的实例执行任何其他操作。

骰子也有一个初始化器,以设置其初始状态。这个初始化器有一个名为generator的参数,它也是RandomNumberGenerator类型的参数。在初始化新的Dice实例时,可以将任何符合此参数的类型的值传递给该参数。

Dice提供了一个实例方法roll,它返回骰子上1到边数之间的整数值。该方法调用生成器的random()方法来创建0.0到1.0之间的新随机数,并使用该随机数创建正确范围内的骰子掷骰值。因为生成器已知采用RandomNumberGenerator,所以它保证调用random()方法。

下面是Dice类如何使用一个线性全等生成器实例作为其随机数生成器来创建一个六边骰子:

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
复制代码

委托

委托是一种设计模式,它允许类或结构将其部分职责移交(或委托)给另一种类型的实例。此设计模式是通过定义封装了委托的职责的协议来实现的,以便保证符合要求的类型(称为委托)能够提供已委托的功能。可以使用委托响应特定的操作,或者从外部源检索数据,而不需要知道该源的底层类型。

下面的例子定义了两个用于基于骰子的棋盘游戏的协议:

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)
}
复制代码

DiceGame协议可以被任何涉及骰子的游戏所采用。

可以使用DiceGameDelegate协议跟踪DiceGame的进程。为了防止强引用循环,委托被声明为弱引用。有关弱引用的信息,请参见类实例之间的强引用循环。将协议标记为仅用于类,让本章后面的SnakesAndLadders类声明其委托必须使用弱引用。一个类专用协议由它从任何对象继承的标记,正如在类专用协议中讨论的那样。

这是一个版本的SnakesAndLadders最初引入控制流。此版本适用于将骰子实例用于其掷骰;采用DiceGame协议;并将其进展通知DiceGameDelegate:

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)
    }
}
复制代码

有关SnakesAndLadders玩法的描述,请参见Break。

这个版本的游戏被封装为一个名为SnakesAndLadders的类,它采用了DiceGame协议。为了符合协议,它提供了一个gettable dice属性和一个play()方法。(dice属性被声明为常量属性,因为它在初始化之后不需要更改,而且协议只要求它必须是可获取的。)

snake和Ladders游戏板设置在类的init()初始化器中进行。所有的游戏逻辑都被移动到协议的play方法中,该方法使用协议的required dice属性来提供它的骰子掷骰值。

请注意,delegate属性被定义为一个可选的DiceGameDelegate,因为玩游戏不需要委托。因为它是可选类型,委托属性被自动设置为nil的初始值。然后,游戏实例化器可以选择将属性设置为合适的委托。因为DiceGameDelegate协议只包含类,所以可以将委托声明为弱委托,以防止引用循环。

DiceGameDelegate提供了三种跟踪游戏进程的方法。这三个方法已经被合并到上面play()方法中的游戏逻辑中,并在新游戏开始、新回合开始或游戏结束时调用。

因为委托属性是可选的DiceGameDelegate,所以play()方法每次调用委托上的方法时都使用可选链接。如果委托属性为nil,这些委托调用将优雅地失败,并且没有错误。如果委托属性是非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")
    }
}
复制代码

dicegamametracker实现了DiceGameDelegate所需的所有三个方法。它使用这些方法来跟踪游戏的回合数。它在游戏开始时将numberOfTurns属性重置为零,每次新回合开始时都将其递增,并在游戏结束后打印出总回合数。

上面所示的gameDidStart(__:)的实现使用game参数来打印关于将要玩的游戏的一些介绍性信息。game参数有一种类型的DiceGame,而不是SnakesAndLadders,因此gameDidStart(_:)只能访问和使用作为DiceGame协议一部分实现的方法和属性。但是,该方法仍然能够使用类型转换来查询底层实例的类型。在本例中,它检查game是否实际上是幕后蛇形梯子的实例,如果是,则打印适当的消息。

gameDidStart(__:)方法还访问传递的游戏参数的dice属性。因为game已知符合DiceGame协议,所以它保证具有dice属性,因此gameDidStart(_:)方法能够访问和打印dice的sides属性,而不管正在玩的是哪种游戏。

下面是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
复制代码

扩展添加协议一致性

即使不能访问现有类型的源代码,也可以扩展现有类型以采用和遵循新协议。扩展可以向现有类型添加新属性、方法和下标,因此能够添加协议可能需要的任何需求。有关扩展的更多信息,请参见扩展。

当将一致性添加到扩展中的实例类型时,类型的现有实例自动采用并符合协议。

例如,这个协议称为TextRepresentable,可以由任何能够表示为文本的类型实现。这可能是对它自身的描述,或者是它当前状态的文本版本:

protocol TextRepresentable {
    var textualDescription: String { get }
}
复制代码

上面的Dice类可以扩展为采用和符合TextRepresentable:

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

这个扩展采用新协议的方式与Dice在其原始实现中提供的方式完全相同。协议名在类型名之后提供,以冒号分隔,并且在扩展的花括号中提供协议的所有需求的实现。

任何骰子实例现在可以处理为TextRepresentable:

let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription)
// Prints "A 12-sided dice"
复制代码

同样,SnakesAndLadders游戏类也可以扩展为采用并符合textreprespresentable协议:

extension SnakesAndLadders: TextRepresentable {
    var textualDescription: String {
        return "A game of Snakes and Ladders with \(finalSquare) squares"
    }
}
print(game.textualDescription)
// Prints "A game of Snakes and Ladders with 25 squares"
复制代码

有条件地符合协议

泛型类型只有在某些条件下才能满足协议的要求,例如当类型的泛型参数符合协议时。通过在扩展泛型类型时列出约束,可以使泛型类型有条件地符合协议。通过编写一个泛型where子句,将这些约束写在要采用的协议名称之后。有关泛型where子句的更多信息,请参见泛型where子句。

下面的扩展使数组实例在存储符合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)
// Prints "[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的实例现在可以在textreprespresentable所需类型的地方使用:

let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// Prints "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。它不是骰子类型的,也不是DiceGame或Hamster类型的,即使幕后的实际实例是其中一种类型的。尽管如此,因为它的类型是TextRepresentable,而TextRepresentable的任何东西都有一个textualDescription属性,所以每次通过循环访问thing.textualDescription是安全的。

协议继承

协议可以继承一个或多个其他协议,并且可以在其继承的需求之上添加更多的需求。协议继承的语法类似于类继承的语法,但是可以列出多个继承协议,用逗号分隔:

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // protocol definition goes here
}
复制代码

下面是一个继承自上面的TextRepresentable协议的协议示例:

protocol PrettyTextRepresentable: TextRepresentable {
    var prettyTextualDescription: String { get }
}
复制代码

这个例子定义了一个新的协议PrettyTextRepresentable,它继承自TextRepresentable。任何采用PrettyTextRepresentable的应用程序都必须满足TextRepresentable执行的所有要求,以及PrettyTextRepresentable执行的附加要求。在本例中,PrettyTextRepresentable添加了一个要求,以提供一个名为prettyTextualDescription的可获取属性,该属性返回一个字符串。

SnakesAndLadders类可以扩展到采用和符合PrettyTextRepresentable:

extension SnakesAndLadders: PrettyTextRepresentable {

    var textualDescription: String {
        return "defalutValue"
    }

    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
    }
}
//textualDescription 的默认实现在最后面会用到
复制代码

这个扩展声明它采用PrettyTextRepresentable协议,并为SnakesAndLadders类型提供了prettyTextualDescription属性的实现。PrettyTextRepresentable的任何东西也必须是TextRepresentable的,因此prettyTextualDescription的实现首先从TextRepresentable协议访问textualDescription属性,开始输出字符串。它附加一个冒号和一个换行符,并以此作为其pretty text 代表。然后遍历棋盘格数组,并添加一个几何形状来表示每个方块的内容:

  • 如果平方的值大于0,则为梯子的底,用▲表示。

  • 如果这平方的值小于0,那么它就是蛇头,用 ▼表示。

  • 否则,平方的值为0,它是一个“自由”的平方,用〇表示。

Class-Only 协议

通过将AnyObject协议添加到协议的继承列表中,可以将协议采用限制为类类型(而不是结构或枚举)。

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    // class-only protocol definition goes here
}
复制代码

在上面的例子中,SomeClassOnlyProtocol只能被类类型采用。编写试图采用SomeClassOnlyProtocol的结构或枚举定义是编译时错误。

当由协议的需求定义的行为假设或要求符合规范的类型具有引用语义而不是值语义时,请使用class-only (仅包含类的)协议。有关引用和值语义的更
多信息,请参见结构和枚举是值类型,类是引用类型。
复制代码

协议组成

要求类型同时符合多个协议可能很有用。您可以使用协议组合将多个协议组合成单个需求。协议组合的行为就像您定义了一个临时本地协议,它具有组合中所有协议的组合需求。协议组合不定义任何新的协议类型。

协议组合具有某种协议和另一种协议的形式。您可以根据需要列出任意多的协议,并使用&分隔它们。除了协议列表之外,协议组合还可以包含一个类类型,您可以使用它来指定所需的超类。

下面是一个例子,它将两个名为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)
// Prints "Happy birthday, Malcolm, you're 21!"
复制代码

在本例中,命名协议对名为name的可获取字符串属性只有一个要求。老化协议对名为age的可获取Int属性只有一个要求。这两种协议都被一个叫做Person的结构所采用。

该示例还定义了wishHappyBirthday(to:)函数。庆祝器参数的类型是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)
// Prints "Hello, Seattle!"
复制代码

函数的参数类型为Location & Named,意思是“Location的子类中符合指定协议的任何类型”。在这种情况下,City满足了这两个要求。

将birthdayPerson传递给beginConcert(in:)函数是无效的,因为Person不是Location的子类。同样,如果您创建了一个Location的子类,它不符合命名协议,那么使用该类型的实例调用beginConcert(in:)也是无效的。

协议一致性检查

您可以使用类型转换中描述的is和as操作符来检查协议一致性,并转换到特定的协议。对协议的检查和转换与对类型的检查和转换遵循完全相同的语法:

  • 如果实例符合协议,则is操作符返回true;如果不符合协议,则返回false。

  • is?(向下转型操作符)返回协议类型的可选值,如果实例不符合该协议,则该值为nil。

  • as!(向下转型操作符)强制向下转换为协议类型,如果downcast没有成功,则触发运行时错误。

这个例子定义了一个叫做HasArea的协议,它只有一个叫做area(可获取double属性)的属性:

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类基于存储的radius属性将area属性作为计算属性实现。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)
]
复制代码

objects数组初始化为包含半径为2个单位的Circle实例的数组文字;用英国的表面积(以平方公里为单位)初始化的Country实例;还有一个有四条腿的Animal实例

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")
    }
}
// Area is 12.5663708
// Area is 243610.0
// Something that doesn't have an area
复制代码

当数组中的对象符合HasArea协议时,as?操作符通过可选绑定解包到名为objectWithArea的常量中。objectWithArea常量的类型是HasArea,因此可以以一种类型安全的方式访问和打印它的area属性。

可选协议要求

您可以为协议定义可选的需求,这些需求不必由符合协议的类型来实现。可选需求由可选修饰符作为协议定义的一部分作为前缀。可选要求是可用的,这样您就可以编写与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类有一个可选的dataSource属性,类型为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
        }
    }
}
复制代码

Counter类将其当前值存储在一个名为count的变量属性中。Counter类还定义了一个名为increment的方法,该方法在每次调用该方法时递增count属性。

increment()方法首先通过在其数据源上查找increment(forCount:)方法的实现来检索增量。increment()方法使用可选链接尝试调用increment(forCount:),并将当前count值作为方法的单个参数传递。

注意,这里有两个级别的可选链接。首先,dataSource可能是nil,因此dataSource的名称后面有一个问号,表示只有当dataSource不是nil时才应该调用increment(forCount:)。其次,即使数据源确实存在,也不能保证它实现了increment(forCount:),因为这是一个可选的需求。这里,增量(forCount:)可能无法实现的可能性也由可选链接处理。只有当increment(forCount:)存在时才会调用increment(forCount:),也就是说,如果它不是nil。这就是为什么increment(forCount:)在它的名字后面也要加上一个问号。

因为对increment(forCount:)的调用可能因为这两个原因之一而失败,所以调用返回一个可选的Int值。即使increment(forCount:)被定义为在反数据源的定义中返回一个不可选的Int值,这也是正确的。即使有两个可选的链接操作,一个接一个,结果仍然封装在一个可选操作中。有关使用多个可选链接操作的更多信息,请参见链接多个级别的链接。

在调用increment(forCount:)之后,它返回的可选Int将使用可选绑定解包到一个名为amount的常量中。如果可选的Int确实包含一个值——也就是说,如果委托和方法都存在,并且方法返回一个值——则将未包装的数量添加到存储的count属性中,并且完成增量。

如果不能从increment(forCount:)方法中检索值—要么因为数据源为nil,要么因为数据源没有实现increment(forCount:)—那么increment()方法尝试从数据源的fixedIncrement属性中检索值。fixedIncrement属性也是一个可选的需求,所以它的值是一个可选的Int值,即使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)
}
// 3
// 6
// 9
// 12
复制代码

上面的代码创建了一个新的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,表示不应该进行进一步的计数。

您可以使用TowardsZeroSource实例和现有Counter实例进行从-4到0的计数。一旦计数器达到零,就不再计数:

counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
    counter.increment()
    print(counter.count)
}
// -3
// -2
// -1
// 0
// 0
复制代码

协议的扩展

可以扩展协议来为符合类型提供方法、初始化器、下标和计算属性实现。这允许您在协议本身上定义行为,而不是在每个类型的单独一致性或全局函数中定义行为。

例如,RandomNumberGenerator协议可以扩展为提供randomBool()方法,该方法使用所需random()方法的结果返回一个随机Bool值:

extension RandomNumberGenerator {
    func randomBool() -> Bool {
        return random() > 0.5
    }
}
复制代码

通过在协议上创建扩展,所有符合标准的类型自动获得此方法实现,而不需要任何额外的修改。

let generator = LinearCongruentialGenerator()
print("Here's a random number: \(generator.random())")
// Prints "Here's a random number: 0.3746499199817101"
print("And here's a random Boolean: \(generator.randomBool())")
// Prints "And here's a random Boolean: false"
复制代码

协议扩展可以向符合规范的类型添加实现,但不能使协议扩展或从另一个协议继承。协议继承总是在协议声明本身中指定。

提供默认实现

您可以使用协议扩展来为该协议的任何方法或计算属性需求提供默认实现。如果符合标准的类型提供了所需方法或属性的自身实现,则将使用该实现而不是扩展提供的实现。

请注意

扩展提供的默认实现的协议需求与可选协议需求是不同的。尽管符合标准的类型不需要提供它们自己的实现,但是可以调用具有默认实现的需求,而不需要可选的链接
复制代码

例如,继承了TextRepresentable协议的PrettyTextRepresentable协议可以提供其所需prettyTextualDescription属性的默认实现,从而简单地返回访问textualDescription属性的结果:

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

//前面已经实现textualDescription的属性,直接获取就行了
复制代码

向协议扩展添加约束

定义协议扩展时,可以指定符合类型的约束,这些约束必须在扩展的方法和属性可用之前满足。通过编写一个泛型where子句,可以在要扩展的协议名称之后编写这些约束。有关泛型where子句的更多信息,请参见泛型where子句。

例如,可以定义Collection协议的扩展,该扩展应用于其元素符合Equatable协议的任何集合。通过将集合的元素约束为Equatable协议(标准库的一部分),您可以使用==和!=操作符检查两个元素之间是否相等。

extension Collection where Element: Equatable {
    func allEqual() -> 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,整数符合Equatable,所以equalNumbers和differentNumbers可以使用allEqual()方法:

print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"
复制代码
请注意

如果符合标准的类型满足多个受约束扩展的需求,这些扩展为相同的方法或属性提供了实现,那么Swift根据最符合的约束来实现。
复制代码

[文章源出处Swift5]docs.swift.org/swift-book/…