协议

未匹配的标注

协议 可以作为方法、属性或者其他的一些特定的任务和功能块的设计蓝图。协议 可以适用于结构体、以及枚举并为它们提供具体的实现或满足特定的需求。任意类型只要满足一个协议的要求,那么我们便称这个类型 遵循 这个协议。

除了要求遵循协议的类型必须提供对应的实现以外,还可以通过 协议扩展 来为协议的遵循者提供默认的或者对其有利的实现。


协议语法

通过很简单的语法便可以为类、结构体以及枚举定义一个协议:

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 关键字作为前缀。即使该类型属性由类实现,需要以 classstatic 关键字作为前缀时,这个规则也适用:

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

以下是一个只有一个实例属性的协议:

protocol FullyNamed {
    var fullName: String { get }
}

FullyNamed 协议要求遵循协议的类型提供一个全名。该协议没有指定有关遵循协议类型的其它任何信息 - 它只指定该类型必须能够为自己提供全名。该协议表示任何遵循 FullyNamed 协议的类型必须有一个名为 fullName 的可读实例属性,它的类型为 String

下面是一个简单结构体的例子,它遵循 FullyNamed 协议:

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

此示例定义了一个名为 Person 的结构体,它代表一个有特定名称的人。它在定义的第一行就声明遵循 FullyNamed 协议。

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")
// ncc1701.fullName 是 "USS Enterprise"

上面这个类通过一个计算只读属性来实现 fullName 属性要求。每个 Starship 类实例都存储一个必须的 name 属性和一个可选的 prefix 属性。fullName 计算只读属性会使用 prefix 值(如果存在),并将其添加到 name 值的前面,以创建星舰的全名。


方法要求

协议可能需要通过遵循类型来实现特定的实例方法和类型方法。这些方法作为协议定义的一部分编写,与普通实例和类型方法完全相同,但没有花括号和方法实现。协议中定义的方法允许使用变量参数,遵循与常规方法相同的规则。但是,我们无法为协议中定义的方法的参数指定默认值。

与类型属性要求一样,当在协议中定义类型方法时,始终使用 static 关键字作为前缀。即使该类型方法要求在由类实现时以 classstatic 关键字为前缀,也是如此:

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())")
// 打印 "Here's a random number: 0.3746499199817101"
print("And another one: \(generator.random())")
// 打印 "And another one: 0.729023776863283"


异变方法要求

有时需要有一个方法来修改(或 异变)它所属的实例。例如,对值类型(即结构体和枚举)的方法,将 mutating 关键字放在方法 func 关键字之前,以指示允许该方法修改它所属的实例以及该实例的任何属性。在实例方法中修改值类型 一文描述了此过程。

如果你定义了一个协议实例方法要求,该要求旨在改变遵循该协议的任何类型的实例,请使用 mutating 关键字作为协议定义的一部分来标记该方法。这使得结构体和枚举能够采用协议并满足该方法要求。

注意

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

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

Togglable 协议在定义的时候,使用了 mutating 关键字来标记 toggle() 方法,以指示该方法在调用时会改变遵循类型实例的状态:

protocol Togglable {
    mutating func toggle()
}

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

下面的示例定义了一个名为 OnOffSwitch 的枚举。此枚举在两个状态之间切换,由枚举情况 onoff 表示。枚举的 toggle 实现被标记为 mutating,以匹配 Togglable 协议的要求:

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) {
        // 下面是构造器的实现
    }
}

使用 required 修饰符可确保你在遵循协议类的所有子类上提供构造器要求的显式或继承实现,以便它们也符合协议。

有关所需构造器的详细信息,请参阅 必需的构造器

注意

你并不需要在使用 final 修饰符标记的类上使用 required 修饰符来标记协议构造器的实现,因为这样的类是不能进行子类化。有关 final 修饰符的更多信息,请参阅 阻止重写

如果子类重写了父类的指定构造器,并且遵循协议实现了构造器要求,则需要使用 requiredoverride 修饰符标记初始化程序实现:

protocol SomeProtocol {
    init()
}

class SomeSuperClass {
    init() {
        // 下面是构造器的实现
    }
}

class SomeSubClass: SomeSuperClass, SomeProtocol {
    // 添加「required」修饰符是因为遵循了 SomeProtocol 协议; 添加「override」修饰符是因为该类继承自 SomeSuperClass
    required override init() {
        // 下面是构造器的实现
    }
}

可失败的构造器要求

我们可以用协议来定义可失败的构造器要求,像 可失败构造器 中所定义的那样。

遵循类型可以用可失败或非可失败的构造器来满足可失败的构造器要求。非可失败的构造器要求必须用非可失败的构造器或隐式展开的可失败的构造器来满足。


将协议作为类型

协议本身并不实现任何功能。不过你创建的任何协议都可以变为一个功能完备的类型在代码中使用。

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

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

注意

因为协议是类型,所以要首字母大写(例如 FullyNamedRandomNumberGenerator),以匹配 Swift 中其他类型的名称格式(例如 IntStringDouble)。

以下是用协议作为类型的示例:

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.01.0 之间随机数,并使用这个随机数在正确的范围内创建一个骰子滚动值。因为 generator 已知遵循 RandomNumberGenerator,所以它保证有一个 random() 方法可以被调用。

这里是 Dice 类使用 LinearCongruentialGenerator 实例作为随机数生成器来创建一个六边形骰子的过程 :

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 类那样,就必须声明其委托为弱引用。我们可以通过对 AnyObject 的继承来实现仅限类的协议,如 仅限类的协议 中所述。

下面是最初在 控制流 中介绍的 蛇与梯子 游戏的一个版本。这个版本通过创建 Dice 实例来遵循 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)
    }
}

有关 蛇与梯子 游戏玩法的说明,请参阅控制流中 Break 小节部分。

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

蛇与梯子 游戏的棋盘设置在类的 init() 构造器中进行。所有游戏逻辑都被移到协议的 play 方法中,该方法使用协议所需的 dice 属性来提供其骰子滚动值。

请注意,因为委托并不是玩这个游戏所必须的,在这里我们会将 delegate 属性定义为 可选的 DiceGameDelegate。同时因为它是一个可选类型,delegate 属性会自动的初始化为 nil。此后,游戏实例化者可以选择将属性设置为合适的委托。因为 DiceGameDelegate 协议是类独有的,所以可以将委托声明为 弱引用 以防止引用循环。

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()
// 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 原类中实现协议的效果是一样的。协议名称写在类型名称后面,用冒号隔开,然后在扩展的大括号内实现协议要求的所有内容。

所有 Dice 实例都可以被视为 TextRepresentable 类型:

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

类似地,SnakesAndLadders 游戏类也可以通过扩展来遵循并实现 TextRepresentable 协议:

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

有条件地遵循协议

泛型类型可能只能在特定条件下满足协议的要求,例如类的泛型参数遵循一个协议。你可以通过在扩展类型时列出条件约束,让泛型类型有条件的遵循一个协议。通过编写一个泛型 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)
// 打印 "[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"

注意

类型不会自动遵循一个协议,即便已经满足这个协议的要求。它们必须显示的申明它们遵循了这个协议。


协议类型的集合

协议可以用作诸如数组或字典之类的集合类型的元素类型,如 将协议作为类型 中所述。这个例子创建了一个 TextRepresentable 数组:

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

现在可以遍历数组中的元素,并打印每个元素的文本描述:

for thing in things {
    print(thing.textualDescription)
}
// 蛇和梯子游戏中 25 个方格
// 12 边 骰子
// 1个名为 Simon 的仓鼠 

请注意,thing 常量是 TextRepresentable 类型。它不是 DiceDiceGame、或 Hamster 类型,但它的实例肯定是其中一种类型。因为它的类型为 TextRepresentable,并且已知任何 TextRepresentable 具有 textualDescription 属性,所以每次遍历访问 thing.textualDescription 都是安全的。


协议继承

协议可以 继承 一个或多个协议,并且可以在其继承的协议的基础上添加更多的要求。协议继承的语法类似于类继承的语法,但是协议继承支持同时继承多个协议,并用逗号隔开:

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // 在这里定义协议
}

下面是一个协议继承前面的 TextRepresentable 协议的例子:

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

在这个例子中定义了一个新协议,PrettyTextRepresentable,它继承自 TextRepresentable。任何遵循 PrettyTextRepresentable 的类型都必须满足 TextRepresentable 中所有的强制要求,加上 PrettyTextRepresentable 中增加的强制要求。在这个例子中,PrettyTextRepresentable 增加了一个返回是 String 类型的名叫 prettyTextualDescription 的可读属性。

可以扩展 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 类提供了 PrettyTextRepresentable 属性的实现方法。因为 PrettyTextRepresentable 继承 TextRepresentable 协议,因此实现 prettyTextualDescription 属性需要先访问 TextRepresentable 协议的 textualDescription 属性。并将 textualDescription 属性拼接一个冒号和换行符,用这作为输出工整文本的开始。他遍历 board 数组并拼接几何符号来表示每个元素的内容:

  • 如果元素的值大于 0 ,使用上升 符号表示。
  • 如果元素的值小于 0 ,使用上升 符号表示。
  • 否则,值为 0 ,这是可用的,用 替换。

现在 prettyTextualDescription 属性输出了一个工整的描述针对 textualDescription 属性:

print(game.prettyTextualDescription)
// 有 25 个正方形的蛇和梯子的游戏:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○


类专属协议

你可以通过将 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!"

在这个例子中,Named 协议有一个名为 name 的可读 String 属性要求。Aged 协议有一个名为 age 的可读 Int 属性要求。两个协议都被名为 Person 的结构体所遵循。

该示例还定义了 wishHappyBirthday(to:) 函数。celebrator 参数的类型是 Named & Aged,这意味着「任何同时遵循 NamedAged 协议的类型。」只要遵循这两个协议,将具体什么类型传递给函数并不重要。

然后该示例创建一个名为 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)
// 打印 "Hello, Seattle!"

beginConcert(in:) 函数接受一个类型为 Location & Named 的参数,这意味着「任何是 Location 的子类,并且遵循 Named 协议的类型。」 在这种情况下,City 满足这两个要求。

birthdayPerson 传递给 beginConcert(in:) 函数是无效的,因为 Person 不是 Location 的子类。同样,如果你创建了一个不遵循 Named 协议的 Location 的子类,那么用该类型的实例调用 beginConcert(in:) 也是无效的。


协议遵循的检查

你可以使用 类型转换 中描述的 isas 运算符来检查协议遵循,和转换成特定协议。检查和转换协议与检查和转换类型的语法相同:

  • 如果实例遵循协议,则 is 运算符返回 true,如果不遵循则返回 false
  • 向下转换运算符的 as? 返回协议类型的可选值,如果实例不遵循该协议,则该值为 nil
  • 向下转换运算符的 as! 强制向下转换为协议类型,如果向下转换不成功则触发运行时错误。

这个例子定义了一个名为 HasArea 的协议,它有一个名为 area 的可读 Double 属性要求:

protocol HasArea {
    var area: Double { get }
}

下面有 CircleCountry 两个类,它们都声明遵循 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 }
}

 CircleCountryAnimal  三个类没有共同的基类。尽管如此,它们的实例化对象都可以被存储在存储类型为  AnyObject 的数组中:

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

这个 objects 数组包含了三个类的实例:半径为2的 Circle 实例、面积为英国国土面积 Country 实例和4条腿的 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 属性的值。

注意数组中的每个对象不会在这个循环过程中被修改。它们仍是 CircleCountryAnimal 类的实例化对象。当它们作为  objectWithArea 常量时,它们只知道自己是否声明并遵循了 HasArea 协议,因此遵守了 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 ,它们使用了不同的方法来从数据源中获取适当的增量值。

注意

严格来讲, 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 来存储当前值。该类还定义了一个 increment 方法,每次调用该方法的时候,将会增加 count 的值。

increment()  方法首先试图使用 increment(forCount:) 方法来得到每次的增量。 increment() 方法使用可选链式调用来尝试调用  increment(forCount:) ,并将当前的 count 值作为参数传入。

这里使用了两层可选链式调用。首先,由于 dataSource 可能为 nil ,因此在 dataSource 后边加上了 ?,以此表明只在 dataSource 非空时才去调用 increment(forCount:) 方法。其次,即使 dataSource 存在,也无法保证其是否实现了 increment(forCount:) 方法,因为这个方法是可选的。因此, increment(forCount:) 方法同样使用可选链式调用进行调用,只有在该方法被实现的情况下才能调用它,所以在  increment(forCount:) 方法后边也加上了 ?

调用 increment(forCount:) 方法在上述两种情形下都有可能失败,所以返回值为 Int 类型。虽然在 CounterDataSource 协议中, increment(forCount:) 的返回值类型是非可选 Int 。另外,即使这里使用了两层可选链式调用,最后的返回结果依旧是单层的可选类型。关于这一点的更多信息,请查阅 连接多层可选链式调用.

在调用 increment(forCount:) 方法后, Int 型的返回值通过可选绑定解包并赋值给常量 amount 。如果可选值确实包含一个数值,也就是说,数据源和方法都存在,数据源方法返回了一个有效值。之后便将解包后的 amount 加到 count 上,增量操作完成。

如果没有从 increment(forCount:) 方法获取到值,可能由于  dataSource 为 nil,或者它并没有实现 increment(forCount:) 方法,那么 increment() 方法将试图从数据源的 fixedIncrement 属性中获取增量。 fixedIncrement 是一个可选属性,因此属性值是一个 Int 值,即使该属性在 CounterDataSource 协议中的类型是非可选的 Int 。

下面的例子展示了 CounterDataSource 的简单实现。 fixedIncrement 类遵循了 CounterDataSource 协议,它实现了可选属性 fixedIncrement ,每次会返回 3 :

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() 方法四次。和预期一样,每次调用都会将 count 的值增加 3.

下面是一个更为复杂的数据源 TowardsZeroSource ,它将使得最后的值变为 0:

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 ,此方法返回 0 ,以此表明之后不应再有增量操作发生。

你可以使用 TowardsZeroSource 实例将 Counter 实例来从 -4 增加到 0。一旦增加到 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())")
// 打印 "Here's a random number: 0.3746499199817101"
print("And here's a random Boolean: \(generator.randomBool())")
// 打印 "And here's a random Boolean: true"

协议扩展可以为符合的类型添加实现,但无法扩展协议本身或是继承其他协议。 协议继承始终在协议自身的声明中指定。

提供默认实现

你可以使用协议扩展来为任何方法或计算属性提供默认实现。如果一个符合的类型本身就实现了协议中要求的方法或属性,那么这个实现会代替协议扩展中的实现。

注意

由协议扩展提供默认实现的协议要求和可选协议要求不同。尽管符合的类型不需要提供任何一种协议的实现,有默认实现的要求在被调用时不需要可选链。

例如, PrettyTextRepresentable 继承的 TextRepresentable 协议,可以为要求的 prettyTextualDescription 属性提供一个默认实现,所以只需要简单地返回访问 textualDescription 属性的结果:

extension PrettyTextRepresentable  {
    var prettyTextualDescription: String {
        return 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

接下来来看下面的两个数组,equalNumbers中所有的元素都相等,
但是differentNumbers则不是。

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

因为数组类型Array和整型Int分别遵循了Collection协议和Equatable协议。所以我们可以分别通过上面两个数组来调用allEqual()这个方法。

print(equalNumbers.allEqual())
// 打印 "true"
print(differentNumbers.allEqual())
// 打印 "false"

注意

如果一个类型遵循了多个具有同名方法或属性的扩展协议,那么 Swift 会优先调用条件约束较多一方的属性或方法。

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/the-swift-progr...

译文地址:https://learnku.com/docs/the-swift-progr...

上一篇 下一篇
讨论数量: 0
发起讨论 只看当前版本


暂无话题~