Swift协议

目录

引入

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

除了指定符合协议的类型必须实现的要求外, 您还可以扩展协议以实现其中的某些要求, 或实现符合协议的类型可以利用的附加功能。

协议语法

定义协议的语法与类、 结构相似。

protocol SomeProtocol {
    // protocol definition goes here
}

自定义类型采用了特定协议, 会将协议名称放在类型名称之后, 使用冒号分隔。

多个协议之间使用逗号分隔。

struct SomeStructure: FirstProtocol, AnotherProtocol {
    // structure definition goes here
}

如果某个类有超类, 需要在协议之前列出超类的名称, 用逗号分隔。

class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
    // class definition goes here
}

属性要求

协议可以要求任何符合要求的类型提供具有特定名称和类型的实例属性或类型属性。 协议并不指定该属性应是存储属性还是计算属性, 它只指定所需的属性名称和类型。 协议还指定了每个属性必须是可获取的还是可获取并可设置的。

如果协议要求某个属性必须是可获取和可设置的, 那么常量存储属性或只读计算属性就不能满足该属性要求。 如果协议只要求一个属性是可获取的, 那么任何类型的属性都可以满足该要求, 如果对自己的代码有用, 属性也可以是可设置的。

属性要求总是以变量属性的形式声明, 前缀为 var 关键字。 可获取和可设置属性在其类型声明后用 { get set } 表示, 可获取属性用 { get } 表示。

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

协议中要求类型属性时, 要使用 static 关键字作为前缀。

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

单属性要求协议示例

protocol FullyNamed {
    var fullName: String { get }
}

符合协议的结构

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

Person 实例都有名为 fullName 的存储属性, 其类型为字符串。 这符合 FullyNamed 协议的唯一要求,意味着 Person 已正确符合该协议。

下面这个类也符合了该协议

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"

方法要求

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

与类型属性要求一样, 在协议中定义类型方法要求时, 必须在其前缀加上 static 关键字。 即使类型方法要求在由类实现时使用类或 static 关键字作为前缀,也是如此。

protocol SomeProtocol {
    static func someTypeMethod()
}

示例

protocol RandomNumberGenerator {
    func random() -> Double
}

随机数生成器协议不对每个随机数的生成方式做任何假设, 它只要求生成器提供生成新随机数的标准方法。

下面是符合该协议的类实现。

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"

变异方法要求

有时一个方法需要修改(或变异)它所属的实例。 对于值类型(即结构和枚举)的实例方法, 可以在方法的 func 关键字前加上 mutating 关键字, 以表示允许该方法修改它所属的实例以及该实例的任何属性。

如果你定义了一个协议实例方法需求, 该方法的目的是对采用该协议的任何类型的实例进行修改, 那么请在该方法上标记 mutating 关键字, 作为协议定义的一部分。 这样, 结构和枚举就可以采用该协议并满足该方法要求。

protocol Togglable {
    mutating func toggle()
}

enum OnOffSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
// lightSwitch is now equal to .on

初始化器要求

协议可以要求符合协议的类型实现特定的初始化器。 可以将这些初始化器作为协议定义的一部分来编写, 编写方式与普通初始化器完全相同, 但不使用大括号或初始化器主体。

protocol SomeProtocol {
    init(someParameter: Int)
}

初始化器要求类实现

可以通过指定初始化器或方便初始化器实现符合协议的类的协议初始化要求。 在这两种情况下,你都必须用 required 修饰符来标记初始化器的实现。

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

如果子类重写超类的初始化器, 同时也实现了协议的要求, 需要同时使用 requiredoverride 修饰符来标记初始化器的实现。

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

作为类型的协议

协议本身实际上并不实现任何功能。 你可以在代码中将协议作为类型使用。

将协议作为类型使用的最常见方式是将协议作为通用约束。 使用通用约束的代码可以与任何符合协议的类型协同工作, 而具体类型则由使用 API 的代码来选择。 例如, 当你调用一个接收参数的函数时, 如果该参数的类型是通用的, 那么调用者就会选择该类型。

不透明类型的代码使用符合协议的类型。 底层类型在编译时是已知的, API 实现会选择该类型, 但该类型的身份对 API 客户端是隐藏的。 使用不透明类型可以防止应用程序接口的实现细节通过抽 象层泄露, 例如, 隐藏函数的特定返回类型,只保证值符合给定的协议。

带有盒式协议类型的代码可以使用运行时选择的任何符合协议的类型。 为了支持这种运行时灵活性, Swift 会在必要时添加一个间接层(称为框), 这需要付出性能代价。 由于这种灵活性, Swift 在编译时并不知道底层类型, 这意味着您只能访问协议所需的成员。 访问底层类型上的任何其他 API 都需要在运行时进行转换。

委托

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

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

下面的类符合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!")
        }
    }
}

运行效果

let tracker = DiceGameTracker()
let game = DiceGame(sides: 6)
game.delegate = tracker
game.play(rounds: 3)
// Started a new game
// Player 2 won round 1
// Player 2 won round 2
// Player 1 won round 3
// Player 2 won!

通过扩展添加协议一致性

即使无法访问现有类型的源代码, 也可以扩展现有类型, 使其采用并符合新协议。 扩展可以为现有类型添加新的属性、 方法和下标, 因此可以添加协议可能要求的任何条件。

任何类型都可以实现下面的协议, 只要能以文本方式表示。

protocol TextRepresentable {
    var textualDescription: String { get }
}

让上面的Dice类扩展符合该协议。

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"

有条件的符合协议

泛型可能只有在特定条件下才能满足协议的要求, 例如当类型的泛型参数符合协议时。 通过在扩展类型时列出限制条件, 可以使通用类型有条件地符合协议。 通过编写泛型 where 子句, 将这些约束写在要采用的协议名称之后。

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

let simonTheHamster = Hamster(name: "Simon")
let somethingTextRepresentable: TextRepresentable = simonTheHamster
print(somethingTextRepresentable.textualDescription)
// Prints "A hamster named Simon"

使用合成实现采用协议

在许多简单的情况下, Swift 可以自动提供 Equatable、Hashable 和 Comparable 的协议一致性。 使用这种合成实现意味着您不必编写重复的模板代码来自行实现协议要求。

Swift 为以下类型的自定义类型提供了 Equatable 的合成实现:

  • 仅具有符合 Equatable 协议的存储属性的结构
  • 只有符合 Equatable 协议的关联类型的枚举
  • 无关联类型的枚举

要接收 == 的合成实现, 请在包含原始声明的文件中声明符合 Equatable 协议, 而无需自行实现 == 操作符。 Equatable 协议提供了 != 的默认实现。

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.")
}
// Prints "These two vectors are also equivalent."

Swift 为以下自定义类型提供了 Hashable 的合成实现:

  • 仅具有符合 Hashable 协议的存储属性的结构
  • 只有符合 Hashable 协议的关联类型的枚举
  • 没有关联类型的枚举

要接收 hash(into:) 的综合实现, 请在包含原始声明的文件中声明符合 Hashable 协议, 而不要自己实现 hash(into:) 方法。

Swift 为没有原始值的枚举提供了 Comparable 的合成实现。 如果枚举有关联类型, 它们必须全部符合 Comparable 协议。 要接收 < 的合成实现,请在包含原始枚举声明的文件中 声明符合 Comparable, 而不要自己实现 < 操作符。

enum SkillLevel: Comparable {
    case beginner
    case intermediate
    case expert(stars: Int)
}
var levels = [SkillLevel.intermediate, SkillLevel.beginner,
              SkillLevel.expert(stars: 5), SkillLevel.expert(stars: 3)]
for level in levels.sorted() {
    print(level)
}
// Prints "beginner"
// Prints "intermediate"
// Prints "expert(stars: 3)"
// Prints "expert(stars: 5)"

协议类型集合

协议可用作存储在数组或字典等集合的类型。

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

协议继承

一个协议可以继承一个或多个其他协议, 并可在其继承的要求之上添加更多要求。 协议继承的语法与类继承的语法相似, 但可以选择列出多个继承协议, 并用逗号分隔.

protocol InheritingProtocol: SomeProtocol, AnotherProtocol {
    // protocol definition goes here
}

继承TextReporesentable协议的示例

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

采用PrettyTextRepresentable协议, 必须符合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
    }
}

仅限类的协议

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

protocol SomeClassOnlyProtocol: AnyObject, SomeInheritedProtocol {
    // class-only protocol definition goes here
}

协议组合

要求一个类型同时符合多个协议可能很有用。 可以通过协议组合将多个协议合并为一个要求。 协议组合的行为就像你定义了一个临时的本地协议, 它具有组合中所有协议的综合要求。 协议组合不会定义任何新的协议类型。

协议组合的形式为 SomeProtocol & AnotherProtocol 。 可以根据需要列出任意多个协议, 并用 “&” 分隔。 除了协议列表外, 协议组合还可以包含一个类类型,您可以用它来指定所需的超类。

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

下面的示例, 结合了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)
    }
}

// in参数:符合Named协议的Location子类
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!"

检查协议一致性

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

  • 如果实例符合协议, is 操作符返回 true; 如果不符合, 则返回 false。
  • 转换操作符的 as? 版本返回协议类型的可选值, 如果实例不符合该协议, 则该值为 nil。
  • 转换操作符的 as! 版本强制下播到协议类型,如果下播不成功,会触发运行时错误。
protocol HasArea {
    var area: Double { get }
}

// 实现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 }
}

// 未实现HasArea协议
class Animal {
    var legs: Int
    init(legs: Int) { self.legs = legs }
}

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

for object in objects {
    // 遍历数组,尝试转换。成功输出area属性值,否则进入else
    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

协议扩展

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

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: true"

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

提供默认实现

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

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

为协议扩展添加约束

在定义协议扩展时, 您可以指定一些约束条件, 要求符合条件的类型在扩展的方法和属性可用之前 必须满足这些约束条件。 你可以通过编写通用 where 子句, 将这些约束写在要扩展的协议名称 之后。

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

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

print(equalNumbers.allEqual())
// Prints "true"
print(differentNumbers.allEqual())
// Prints "false"