Skip to content

定义符合类型的实现要求

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

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

协议语法

您可以像定义类、结构体和枚举一样定义协议:

swift
protocol SomeProtocol {
    // protocol definition goes here
}

自定义类型通过在其类型名称后放置协议名称(两者之间用冒号分隔)来声明它们采用了一种特定的协议。可以列出多个协议,它们之间用逗号分隔:

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

如果一个类有超类,则在列出该类采用的协议之前先列出超类名称,两者之间用逗号分隔:

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

注意

由于协议是类型,因此应以大写字母开头(如 FullyNamedRandomNumberGenerator)以匹配 Swift 中其他类型的名称(如 IntStringDouble)。

属性要求

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

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

属性要求总是声明为可变属性,并以 var 关键字前缀。可获取和可设置的属性通过在其类型声明后写入 { get set } 来表示,可获取的属性通过在其后写入 { get } 来表示。

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

在定义协议中的类型属性要求时,始终使用 static 关键词进行前缀。即使类型属性要求在由类实现时可以使用 classstatic 关键词前缀,此规则仍然适用:

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

这是一个包含单一实例属性要求的协议示例:

swift
protocol FullyNamed {
    var fullName: String { get }
}

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

这是一个采用并遵循 FullyNamed 协议的简单结构示例:

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

这个例子定义了一个名为 Person 的结构,它代表了一个特定命名的人。它声明了采用 FullyNamed 协议作为其定义的第一行的一部分。

每个 Person 的实例都有一个单一的存储属性 fullName,其类型为 String。这符合 FullyNamed 协议的单一要求,这意味着 Person 正确地遵循了该协议。(如果协议要求未得到满足,Swift 将在编译时报告错误。)

这是一个更复杂的类,它也采用了并遵循了 FullyNamed 协议:

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 类实例存储一个必需的 name 和一个可选的 prefixfullName 属性使用 prefix 的值(如果存在),并在 name 的开头将其添加,以创建星舰的完整名称。

方法要求

协议可以要求采用该协议的类型实现特定的实例方法和类型方法。这些方法的编写方式与普通实例方法和类型方法完全相同,但不需要花括号或方法体。允许使用可变参数,但需遵循与普通方法相同的规定。然而,在协议的定义中,无法为方法参数指定默认值。

与类型属性要求一样,当你在协议中定义类型方法要求时,总是需要使用 static 关键词进行前缀。即使在类实现类型方法要求时会使用 classstatic 关键词,这一点也同样适用:

swift
protocol SomeProtocol {
    static func someTypeMethod()
}

以下示例定义了一个需要实现单一实例方法的协议:

swift
protocol RandomNumberGenerator {
    func random() -> Double
}

该协议,RandomNumberGenerator,要求任何遵循该协议的类型必须具有一个名为 random 的实例方法,每当调用该方法时,它会返回一个 Double 值。尽管该值不是协议的一部分,但假定该值将是一个从 0.0 开始到(但不包括)1.0 的数字。

RandomNumberGenerator 协议并没有对每个随机数是如何生成的做出任何假设——它仅仅要求生成器提供一种生成新随机数的标准方法。

这是一个实现了并符合 RandomNumberGenerator 协议的类的实现。该类实现了一个被称为线性同余生成器的伪随机数生成算法:

swift
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 关键字。这使结构体和枚举能够采用该协议并满足该方法需求。

注意

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

以下示例定义了一个名为 Togglable 的协议,该协议定义了一个单一的实例方法要求,称为 toggle。如其名称所示,toggle() 方法旨在切换或反转任何符合类型的当前状态,通常通过修改该类型的属性来实现。

toggle() 方法作为 mutating 协议定义的一部分被标记为 Togglable 关键字,以表明当调用该方法时,该方法期望会改变符合该协议的实例的状态:

swift
protocol Togglable {
    mutating func toggle()
}

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

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

swift
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

初始化器要求

协议可以要求特定的初始化器由遵循类型的实现。您可以在协议定义中以与普通初始化器完全相同的方式编写这些初始化器,但不需要花括号或初始化器主体:

swift
protocol SomeProtocol {
    init(someParameter: Int)
}

遵循协议的初始化器要求的类实现

您可以在遵循协议的类中将协议初始化器要求实现为指定的初始化器或方便的初始化器。在两种情况下,您都必须使用 required 修饰符标记初始化器的实现:

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

使用 required 标记可确保您为符合类的所有子类提供显式或继承的初始化要求实现,从而使它们也符合该协议。

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

注意

对于标记了 final 修改符的类,不需要在协议初始化器实现上标记 required 修改符,因为最终类不能被子类化。有关 final 修改符的更多信息,请参见防止重写。

如果子类重写了超类的指定初始化器,并且还实现了协议中的匹配初始化器要求,则将初始化器实现标记为 requiredoverride 修改符:

swift
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 的客户端是隐藏的。使用不透明类型可以让您防止 API 实现细节通过抽象层泄露——例如,通过隐藏函数的具体返回类型,并仅保证值符合给定的协议。

具有封装协议类型的代码可以与任何在运行时选择的符合协议的类型一起工作。为了支持这种运行时的灵活性,Swift 在必要时会增加一层间接性,这被称为封装,这会带来性能成本。由于这种灵活性,Swift 在编译时不知道底层类型,这意味着您只能访问协议所需的成员。访问底层类型的其他 API 需要在运行时进行转换。

对于有关使用协议作为通用约束的信息,请参阅通用。对于有关不透明类型以及封装协议类型的信息,请参阅不透明和封装协议类型。

委托

委托是一种设计模式,它使一个类或结构能够将某些职责委派(或委托)给另一类型的实例。这种设计模式通过定义一个协议来封装委托的职责,使得遵循该协议的类型(称为委托)被保证提供已被委派的功能。委托可以用于响应特定的操作,或者从外部源检索数据而无需知道该源的底层类型。

以下示例定义了一个骰子游戏和一个用于跟踪游戏进度的嵌套协议:

swift
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 类实现了一个游戏,每个玩家轮流掷骰子,掷出最高数字的玩家赢得该轮。它使用了本章前面示例中的线性同余生成器来生成骰子掷出的随机数。

DiceGame.Delegate 协议可以用于跟踪骰子游戏的进度。由于 DiceGame.Delegate 协议总是用于骰子游戏的上下文中,因此它嵌套在 DiceGame 类中。协议可以嵌套在结构和类等类型声明中,只要外部声明不是泛型声明。有关嵌套类型的更多信息,请参阅嵌套类型。

为了防止强引用循环,代理被声明为弱引用。有关弱引用的更多信息,请参阅强引用循环之间的类实例。通过将协议标记为类专用,可以让 DiceGame 类声明其代理必须使用弱引用。类专用协议通过其从 AnyObject 继承,如在类专用协议中所述。

DiceGame.Delegate 提供了三种用于跟踪游戏进度的方法。这些三种方法被整合到上述的 play(rounds:) 方法中。DiceGame 类在游戏开始、新回合开始或游戏结束时会调用其代理方法。

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

This next example shows a class called DiceGameTracker,which adopts the DiceGame.Delegate protocol:

swift
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!")
        }
    }
}

The DiceGameTracker 类实现了 DiceGame.Delegate 协议所需的所有三个方法。它使用这些方法在新游戏开始时清空两个玩家的得分,在每轮结束时更新他们的得分,并在游戏结束时宣布获胜者。

Here's how DiceGameDiceGameTracker look in action:

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

添加协议一致性扩展

您可以扩展现有的类型,使其采用并符合新的协议,即使您无法访问现有类型的源代码也是如此。扩展可以为现有类型添加新的属性、方法和下标,因此能够添加协议可能要求的任何需求。有关扩展的更多信息,请参见扩展。

注意

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

例如,这个称为 TextRepresentable 的协议可以由任何具有以文本形式表示自身方式的类型实现。这可能是其描述,或其当前状态的文本版本:

swift
protocol TextRepresentable {
    var textualDescription: String { get }
}

上面的 Dice 类可以扩展以采用并符合 TextRepresentable

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

该扩展以与如果 Dice 在其原始实现中提供时完全相同的方式采用新的协议。协议名称在类型名称后以冒号分隔,并在扩展的大括号内提供了协议的所有要求的实现。

现在,任何 Dice 实例都可以被视为 TextRepresentable

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

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

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