Published on

协议 in Swift

Authors
  • Name
    Twitter

什么是协议

协议(Protocol) 定义了满足特定任务或功能所需的方法、属性和其他要求的蓝图。类、结构体或枚举可以 采用(adopt) 该协议,并提供协议要求的具体实现。任何满足协议要求的类型都被称为 遵循(conform) 该协议。

有哪些类型可以 遵循 (conform) 某个协议

类、结构体、枚举

什么是协议扩展,有什么作用

除了声明所遵循类型必须实现的要求之外,你还可以通过对协议进行扩展来实现一部分要求或附加功能,这样遵循协议的类型就能够使用这些功能。

协议语法

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

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

要让自定义类型遵循某个协议,在定义类型时,需要在类型名称后加上协议名称,中间以冒号(:)分隔。遵循多个协议时,各协议之间用逗号(,)分隔:

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // 这里是结构体的定义部分
}

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

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

注意 由于协议是类型,它们的名称应以大写字母开头(如 FullyNamed 和 RandomNumberGenerator),以与 Swift 中其他类型的命名规范(如 Int、String 和 Double)保持一致。

属性要求

协议可以要求遵循协议的类型提供特定名称和类型的实例属性或类型属性。

可以定义需要遵循的实例属性和类型属性

协议不指定属性是存储属性还是计算属性,它只指定属性的名称和类型。

所以,协议是不能指定属性是存储属性还是计算属性的。但是它可以指定属性的名称和类型。

此外,协议还指定属性是可读的还是可读写的。

通过什么样的方式呢? 协议总是用 var 关键字来声明变量属性,在类型声明后加上 { set get } 来表示属性是可读可写的,可读属性则用 { get } 来表示:

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

不能使用 let 在 protocol 中声明属性

协议中的 var 是一个占位符,表示这是一个属性要求,而不是指实现必须使用 var. 实现类型可以自由选择:

  • { get } 属性可以用 let var 或 计算属性实现。
  • { get set } 属性必须用 var 或读写计算属性实现。

在协议中定义类型属性时,总是使用 static 关键字作为前缀。当一个类遵循协议时,除了 static 关键字,还可以使用 class 关键字来声明类型属性:

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

如下所示,这是一个只含有一个实例属性要求的协议:

protocol FullyNamed {
    var fullName: String { get }
}

FullyNamed 协议除了要求遵循协议的类型提供 fullName 属性外,并没有其他特别的要求。这个协议表示,任何遵循 FullyNamed 的类型,都必须有一个可读的 String 类型的实例属性 fullName。 下面是一个遵循 FullyNamed 协议的简单结构体:

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

这个例子中定义了一个叫做 Person 的结构体,用来表示一个具有名字的人。它在定义的第一行声明了它遵循 FullyNamed 协议。 每个 Person 实例都有一个 String 类型的存储属性 fullName。这正好满足了 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"

Starship 类把 fullName 作为只读的计算属性来实现。每一个 Starship 类的实例都有一个名为 name 的非可选属性和一个名为 prefix 的可选属性。当 prefix 存在时,计算属性 fullName 会将 prefix 插入到 name 之前,从而得到一个带有 prefix 的 fullName。

方法要求

协议可以要求遵循协议的类型实现某些指定的实例方法和类方法。这些方法的编写方式与普通实例方法和类型方法完全相同,都写在协议定义的一部分中,但没有大括号或方法主体。协议允许使用可变参数,和普通方法的定义方式相同。但是,不能在协议定义中为方法参数指定默认值。

正如属性要求中所述,在协议中定义类方法的时候,总是使用 static 关键字作为前缀。即使在类中实现时,类方法要求使用 class 或 static 作为关键字前缀,这条规则仍然适用:

protocol SomeProtocol {
    static func someTypeMethod()
}

下面的例子定义了一个只含有一个实例方法的协议:

protocol RandomNumberGenerator {
    func random() -> Double
}

RandomNumberGenerator 协议要求遵循协议的类型必须拥有一个名为 random,返回值类型为 Double 的实例方法。尽管这里并未指明,但是我们假设返回值是从 0.0 到(但不包括)1.0。 RandomNumberGenerator 协议并不关心每一个随机数是怎样生成的 —— 它只要求生成器提供一种标准的方式来生成新的随机数。 这里有一个采用并遵循 RandomNumberGenerator 协议的类。该类实现了一个叫做 线性同余生成器(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关键字作为方法的前缀,写在func关键字之前,表示可以在该方法中修改它所属的实例与实例的任意属性的值。

是不是只有值类型才存在这种用法?

在 方法 -> 从实例方法内部修改值类型 中有详细的说明。

  1. 从实例方法内部修改值类型
  2. 在mutating方法中给self赋值

类型方法: 1.static 2.class 允许子类覆盖父类对该方法的实现

在类型方法的主体内部,隐式的 self 属性指的是类型本身,而不是该类型的实例。 这意味着你可以使用 self 来区分类型属性和类型方法形参,正如你在实例属性和实例方法形参中所做的那样。

如何理解这句话

class Counter {
    static var count: Int = 0 // 类型属性
    
    static func increment(count: Int) -> Int {
        // self.count 是类型属性,count 是形参
        self.count += count
        return self.count
    }
}

print(Counter.increment(count: 5)) // 输出 5
print(Counter.increment(count: 3)) // 输出 8

如果你在协议中定义了一个实例方法,该方法会改变遵循该协议的类型的实例,那么在定义协议时需要在方法前加 mutating 关键字。这使得结构体和枚举能够遵循此协议并满足此方法要求。

注意 实现协议中的 mutating 方法时,若是类类型,则不用写 mutating 关键字。mutating 关键字只用于结构体和枚举。

如下所示,Togglable 协议只定义了一个名为 toggle 的实例方法。顾名思义,toggle() 方法将改变实例属性,从而切换遵循该协议类型的实例的状态。 toggle() 方法在定义的时候,使用 mutating 关键字标记,这表明当它被调用时,该方法将会改变遵循协议的类型的实例:

protocol Togglable {
    mutating func toggle()
}

当使用枚举或结构体来实现 Togglable 协议时,需要提供一个被标记为 mutating 的 toggle() 方法。 下面定义了一个名为 OnOffSwitch 的枚举。这个枚举在两种状态之间进行切换,用枚举成员 On 和 Off 表示。枚举的 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 修饰符可以确保所有子类也必须提供此构造器实现,从而也能遵循协议。 关于 required 构造器的更多内容,参考 必要构造器

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

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

protocol SomeProtocol {
    init()
}


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


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

可失败构造器要求

协议还可以为遵循协议的类型定义可失败构造器要求,详见 可失败构造器。 遵循协议的类型可以通过可失败构造器(init?)或非可失败构造器(init)来满足协议中定义的可失败构造器要求。 协议中定义的非可失败构造器要求可以通过非可失败构造器(init)或隐式解包可失败构造器(init!)来满足。

协议作为类型

协议本身并不实现任何功能。尽管如此,你仍然可以在代码中将协议用作类型。 最常见的将协议用作类型的方式是将其用作泛型约束。具有泛型约束的代码可以与任何符合该协议的类型一起工作,具体的类型由使用该 API 的代码选择。例如,当你调用一个函数并传入一个参数,而该参数的类型是泛型时,调用者会选择具体的类型。 在代码中使用不透明类型时,可以与某个符合该协议的类型一起工作。底层类型在编译时是已知的,API 实现会选择该类型,但该类型的身份对 API 的使用方是隐藏的。使用不透明类型可以防止 API 的实现细节泄露到抽象层之外 —— 例如,通过隐藏函数的具体返回类型,并仅保证该值符合给定的协议。 代码使用装箱(boxed)的协议类型时,可以与任何在运行时选择的、符合该协议的类型一起工作。为了支持这种运行时的灵活性,Swift 在必要时会添加一个间接层 —— 称为 箱子(box),这会带来性能开销。由于这种灵活性,Swift 在编译时无法知道底层类型,这意味着你只能访问协议所要求的成员。要访问底层类型的任何其他 API,都需要在运行时进行类型转换。 关于使用协议作为泛型约束的信息,参考 泛型。关于不透明类型和装箱协议类型的信息,参考 不透明类型和封装协议类型。

代理

代理 (Delegate) 是一种设计模式,它允许类或结构体将一些需要它们负责的功能代理给其他类型的实例。 代理模式的实现很简单:定义协议来封装那些需要被代理的功能,这样就能确保遵循协议的类型能提供这些功能。 代理模式可以用来响应特定的动作,或者接收外部数据源提供的数据,而无需关心外部数据源的类型。 下面的例子定义了两个基于骰子游戏的协议: 以下示例定义了一个骰子游戏,以及一个用于跟踪游戏进度的嵌套协议:

class DiceGame {
    let sides: Int
    let generator = LinearCongruentialGenerator()
    weak var delegate: Delegate?

    init(sides: Int) {
        self.sides = sides
    }

    func roll() -> Int {
        return Int(generator.random() * Double(sides)) + 1
    }

    func play(rounds: Int) {
        delegate?.gameDidStart(self)
        for round in 1...rounds {
            let player1 = roll()
            let player2 = roll()
            if player1 == player2 {
                delegate?.game(self, didEndRound: round, winner: nil)
            } else if player1 > player2 {
                delegate?.game(self, didEndRound: round, winner: 1)
            } else {
                delegate?.game(self, didEndRound: round, winner: 2)
            }
        }
        delegate?.gameDidEnd(self)
    }

    protocol Delegate: AnyObject {
        func gameDidStart(_ game: DiceGame)
        func game(_ game: DiceGame, didEndRound round: Int, winner: Int?)
        func gameDidEnd(_ game: DiceGame)
    }

    
}

DiceGame.Delegate 协议可用于跟踪骰子游戏的进度。由于 DiceGame.Delegate 协议总是在骰子游戏的上下文中使用,因此它被嵌套在 DiceGame 类内部。协议可以嵌套在类型声明(如结构体和类)内部,只要外部声明不是泛型。关于嵌套类型的更多信息,参见 嵌套类型。 为了防止强引用循环,代理被声明为弱引用。关于弱引用的更多信息,参见 类实例之间的强引用循环。将协议标记为 class-only 允许 DiceGame 类声明其代理必须使用弱引用。一个 class-only 协议通过继承自 AnyObject 来标记,如 类专属的协议 中所述。

DiceGame.Delegate 提供了三个方法来跟踪游戏的进度。这三个方法被整合到上面的 play(rounds:) 方法的游戏逻辑中。当新游戏开始、新回合开始或游戏结束时,DiceGame 类会调用它的代理方法。

因为 delegate 属性是 可选的 DiceGame.Delegate,所以 play(rounds:) 方法在每次调用代理方法时都使用可选链,如 可选链式调用(Optional Chaining) 中所述。如果 delegate 属性为 nil,这些代理调用将被忽略。如果 delegate 属性不为 nil,则会调用代理方法,并将 DiceGame 实例作为参数传递。

下一个示例展示了一个名为 DiceGameTracker 的类,它遵循了 DiceGame.Delegate 协议:

class DiceGameTracker: DiceGame.Delegate {
    var playerScore1 = 0
    var playerScore2 = 0
    func gameDidStart(_ game: DiceGame) {
        print("Started a new game")
        playerScore1 = 0
        playerScore2 = 0
    }
    func game(_ game: DiceGame, didEndRound round: Int, winner: Int?) {
        switch winner {
            case 1:
                playerScore1 += 1
                print("Player 1 won round \(round)")
            case 2: playerScore2 += 1
                print("Player 2 won round \(round)")
            default:
                print("The round was a draw")
        }
    }
    func gameDidEnd(_ game: DiceGame) {
        if playerScore1 == playerScore2 {
            print("The game ended in a draw.")
        } else if playerScore1 > playerScore2 {
            print("Player 1 won!")
        } else {
            print("Player 2 won!")
        }
    }
}

DiceGameTracker 类实现了 DiceGame.Delegate 协议要求的所有三个方法。它通过这些方法在新游戏开始时将两个玩家的分数清零,在每轮结束时更新他们的分数,以及在游戏结束时宣布获胜者。 以下是 DiceGame 和 DiceGameTracker 的实际运行情况:

let tracker = DiceGameTracker()
let game = DiceGame(sides: 6)
game.delegate = tracker
game.play(rounds: 3)
// 开始新游戏
// Player 2 won round 1
// Player 2 won round 2
// Player 1 won round 3
// Player 2 won!

在扩展里添加协议遵循

即便无法修改源代码,你依然可以通过扩展令已有类型采用并遵循协议。扩展可以为已有类型添加属性、方法、下标以及构造器,因此可以符合协议中可能需要的任意要求。关于扩展的更多详情,参见 扩展。

注意 当一个协议的遵循被添加到实例类型的扩展中时,现有的实例会自动采用并遵循该协议。

例如下面这个 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)
// 打印 “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”

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

使用合成实现来遵循协议

Swift 可以在很多简单场景下自动提供遵循 Equatable、Hashable 和 Comparable 协议的实现。在使用这些合成实现之后,无需再编写重复的样板代码来实现这些协议所要求的方法。

Swift 为以下几种自定义类型提供了 Equatable 协议的合成实现: 只包含遵循 Equatable 协议的存储属性的结构体 只包含遵循 Equatable 协议的关联类型的枚举 没有任何关联类型的枚举

在包含类型原始声明的文件中声明对 Equatable 协议的遵循,可以得到 == 操作符的合成实现,且无需自己编写任何关于 == 的实现代码。Equatable 协议同时包含 != 操作符的默认实现。

下面的例子中定义了一个 Vector3D 结构体来表示一个类似 Vector2D 的三维向量 (x, y, z)。由于 x、y 和 z 都是满足 Equatable 的类型,Vector3D 可以得到等价运算符的合成实现。

struct Vector3D: Equatable {
    var x = 0.0, y = 0.0, z = 0.0
}


let twoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
let anotherTwoThreeFour = Vector3D(x: 2.0, y: 3.0, z: 4.0)
if twoThreeFour == anotherTwoThreeFour {
    print("These two vectors are also equivalent.")
}
// 打印 "These two vectors are also equivalent."

Swift 为以下几种自定义类型提供了 Hashable 协议的合成实现: 只包含遵循 Hashable 协议的存储属性的结构体 只包含遵循 Hashable 协议的关联类型的枚举 没有任何关联类型的枚举 在包含类型原始声明的文件中声明对 Hashable 协议的遵循,可以得到 hash(into:) 的合成实现,且无需自己编写任何关于 hash(into:) 的实现代码。

Swift 为没有原始值的枚举类型提供了 Comparable 协议的合成实现。如果枚举类型包含关联类型,那这些关联类型也必须同时遵循 Comparable 协议。在包含原始枚举类型声明的文件中声明其对 Comparable 协议的遵循,可以得到 < 操作符的合成实现,且无需自己编写任何关于 < 的实现代码。Comparable 协议同时包含 <=、> 和 >= 操作符的默认实现。 下面的例子中定义了 SkillLevel 枚举类型,其中定义了 beginner(初学者)、intermediate(中级)和 expert(专家)三种类型,专家类型会由额外的 stars(星级)数量来进行排名。

enum SkillLevel: Comparable {
    case beginner
    case intermediate
    case expert(stars: Int)
}

var levels = [SkillLevel.intermediate, SkillLevel.beginner, SkillLevel.expert(starts:5), SkillLevel.expert(stars: 3)]

for level in levels.sorted() {
    print(level)
}

// 打印 “beginner”
// 打印 “intermediate”
// 打印 “expert(stars: 3)”
// 打印 “expert(stars: 5)”

协议类型的集合

协议类型可以在数组或者字典这样的集合中使用,在 协议作为类型 提到了这样的用法。下面的例子创建了一个元素类型为 TextRepresentable 的数组:

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

现在可以遍历 things 数组,并打印每个元素的文本表示:

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 类型而不是 Dice,DiceGame,Hamster 等类型,即使实例在幕后确实是这些类型中的一种。由于 thing 是 TextRepresentable 类型,任何 TextRepresentable 的实例都有一个 textualDescription 属性,所以在每次循环中可以安全地访问 thing.textualDescription。

协议的继承

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

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

如下所示,PrettyTextRepresentable 协议继承了上面提到的 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
    }
}

上述扩展令 SnakesAndLadders 遵循了 PrettyTextRepresentable 协议,并提供了协议要求的 prettyTextualDescription 属性。每个 PrettyTextRepresentable 类型同时也是 TextRepresentable 类型,所以在 prettyTextualDescription 的实现中,可以访问 textualDescription 属性,然后拼接上冒号和换行符,接着遍历数组中的元素,拼接一个几何图形来表示每个棋盘方格的内容: 当从数组中取出的元素的值大于 0 时,用 ▲ 表示。 当从数组中取出的元素的值小于 0 时,用 ▼ 表示。 当从数组中取出的元素的值等于 0 时,用 ○ 表示。 任意 SankesAndLadders 的实例都可以使用 prettyTextualDescription 属性来打印一个漂亮的文本描述:

print(game.prettyTextualDescription)
// 一个有 25 个方格的蛇梯棋(Snakes and Ladders)游戏:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○

类专属的协议

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

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

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

注意 当协议定义的要求需要遵循协议的类型必须是引用语义而非值语义时,应该使用类类型专属协议。关于引用语义和值语义的更多内容,参见 结构体和枚举是值类型 和 类是引用类型。

协议组合

要求一个类型同时遵循多个协议是很有用的。你可以使用 协议组合 来组合多个协议到一个要求里。协议组合的行为就和你定义的临时局部协议一样,拥有组合中所有协议的需求。协议组合不定义任何新的协议类型。

协议组合使用 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!”

在这个例子中,Named 协议包含 String 类型的 name 属性。Aged 协议包含 Int 类型的 age 属性。Person 结构体遵循了这两个协议。 这个例子中也定义了一个 wishHappyBirthday(to:) 函数,其参数 celebrator 的类型为 Named & Aged,这意味着“任何同时遵循 Named 和 Aged 协议的类型”。它不关心参数的具体类型,只要参数遵循这两个协议即可。 这个例子随后创建了一个名为 birthdayPerson 的 Person 的实例,作为参数传递给了 wishHappyBirthday(to:) 函数。因为 Person 同时遵循这两个协议,所以这个参数合法,wishHappyBirthday(to:) 函数就能打印生日问候语。

这里有一个例子:将 Location 类和前面的 Named 协议进行组合:

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 的子类。同理,如果你新建一个类继承于 Location,但是没有遵循 Named 协议,用这个类的实例去调用 beginConcert(in:) 函数也是不合法的。

检查是否遵循协议

你可以使用 类型转换 中描述的 is 和 as 操作符来检查是否遵循某协议,并且可以类型转换到指定的协议。检查和转换协议的语法与检查和转换类型是完全一样的:

  • is 用来检查实例是否遵循某个协议,若遵循则返回 true,否则返回 false。
  • as? 返回一个可选值,当实例遵循某个协议时,返回类型为协议类型的可选值,否则返回 nil。
  • as! 将实例强制向下转换到某个协议类型,如果强转失败,将触发运行时错误。 下面的例子定义了一个 HasArea 协议,该协议定义了一个 Double 类型的可读属性 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)
]

objects 数组使用字面量初始化,数组包含一个 radius 为 2 的 Circle 的实例,一个保存了英国国土面积的 Country 实例和一个 legs 为 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 属性可以类型安全地被访问和打印。 objects 数组中实际的元素的类型并不会因为强转而丢失类型信息,它们仍然是 Circle,Country,Animal 类型。然而,当它们被赋值给 objectWithArea 常量时,只被视为 HasArea 类型,因此只有 area 属性能够被访问。

可选协议要求

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

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

协议中的可选要求可通过可选链式调用来使用,因为遵循协议的类型可能没有实现这些可选要求。类似 someOptionalMethod?(someArgument) 这样,你可以在可选方法名称后加上 ? 来调用可选方法。和可选链式有关的详细内容,可参见 可选链式调用(Optional Chaining)。

下面的例子定义了一个名为 Counter 的用于整数计数的类,它使用外部的数据源来提供每次的增量。数据源由 CounterDataSource 协议定义,它包含两个可选要求:

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

CounterDataSource 协议定义了一个可选方法 increment(forCount:) 和一个可选属性 fiexdIncrement,它们使用了不同的方法来从数据源中获取适当的增量值。

注意 严格来讲,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:) 存在时——也就是说,如果它不是 nil ——才会调用 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 的简单实现。ThreeSource 类遵循了 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() 方法 4 次。正如预期一样,每次调用都会将 count 的值增加 3. 下面是一个更为复杂的数据源 TowardsZeroSource,它将使得 Counter 实例的 count 属性的值增加或减少,最终变为 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.37464991998171”
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 协议(Swift 标准库的一部分), 你可以使用 == 和 != 操作符来检查两个元素的等价性和非等价性。

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

注意 如果一个遵循类型满足了为同一方法或属性提供实现的多个有限制条件的协议扩展的要求,Swift 会使用最贴合限制的实现。