协议
协议定义了一个蓝图,规定了用来实现某一特定任务或者功能的方法、属性,以及其他需要的东西。类、结构体或枚举都可以采纳协议,并为协议定义的这些要求提供具体实现。某个类型能够满足某个协议的要求,就可以说该类型“符合”这个协议。
除了采纳协议的类型必须实现的要求外,还可以对协议进行扩展,通过扩展来实现一部分要求或者实现一些附加功能,这样采纳协议的类型就能够使用这些功能。
协议语法
使用protocol
定义协议。协议的定义方式与类、结构体和枚举的定义非常相似。要让自定义类型采纳某个协议,在定义类型时,需要在类型名称后加上协议名称,中间以冒号(:
)分隔。采纳多个协议时,各协议之间用逗号(,
)分隔。拥有父类的类在采纳协议时,应该将父类名放在协议名之前,以逗号分隔。
属性要求
协议可以要求采纳协议的类型提供特定名称和类型的实例属性或类型属性。协议不指定属性是存储型属性还是计算型属性,它只指定属性的名称和类型。此外,协议还指定属性是只读的还是可读可写的。
如果协议要求属性是可读可写的,那么该属性不能是常量属性或只读的计算型属性。如果协议只要求属性是只读的,那么该属性不仅可以是只读的,如果代码需要的话,还可以是可写的。
协议通常用var
关键字来声明变量属性,在类型声明后加上 { set get }
来表示属性是可读可写的,只读属性则用{ get }
来表示。
在协议中定义类型属性时,总是使用static
关键字作为前缀。当类类型采纳协议时,除了static
关键字,还可以使用class
关键字来声明类型属性。
方法要求
协议可以要求采纳协议的类型实现某些指定的实例方法或类方法。这些方法作为协议的一部分,像普通方法一样放在协议的定义中,但是不需要大括号和方法体。可以在协议中定义具有可变参数的方法,和普通方法的定义方式相同。但是,不支持为协议中的方法的参数提供默认值。
正如属性要求中所述,在协议中定义类方法的时候,总是使用static
关键字作为前缀。当类类型采纳协议时,除了static
关键字,还可以使用class
关键字作为前缀。
在上面的代码中,实现了“线性同余生成器(linear congruential generator)”的伪随机数算法。
@H_301_281@mutating方法要求有时需要在方法中改变方法所属的实例。例如,在值类型(即结构体和枚举)的实例方法中,将mutating
关键字作为方法的前缀,写在func
关键字之前,表示可以在该方法中修改它所属的实例以及实例的任意属性的值。
如果你在协议中定义了一个实例方法,该方法会改变采纳该协议的类型的实例,那么在定义协议时需要在方法前加mutating
关键字。这使得结构体和枚举能够采纳此协议并满足此方法要求。
注意,实现协议中的mutating
方法时,若是类类型,则不用写mutating
关键字。而对于结构体和枚举,则必须写mutating
关键字。
构造器要求
协议可以要求采纳协议的类型实现指定的构造器。你可以像编写普通构造器那样,在协议的定义里写下构造器的声明,但不需要写花括号和构造器的实体。
构造器要求在类中的实现
你可以在采纳协议的类中实现构造器,无论是作为指定构造器,还是作为便利构造器。无论哪种情况,你都必须为构造器实现标上required
修饰符。使用required
修饰符可以确保所有子类也必须提供此构造器实现,从而也能符合协议。
如果类已经被标记为final
,那么不需要在协议构造器的实现中使用required
修饰符,因为final
类不能有子类。
如果一个子类重写了父类的指定构造器,并且该构造器满足了某个协议的要求,那么该构造器的实现需要同时标注required
和override
修饰符。
协议还可以为采纳协议的类型定义可失败构造器要求。采纳协议的类型可以通过可失败构造器(init?
)或非可失败构造器(init
)来满足协议中定义的可失败构造器要求。协议中定义的非可失败构造器要求可以通过非可失败构造器(init
)或隐式解包可失败构造器(init!
)来满足。
协议作为类型
尽管协议本身并未实现任何功能,但是协议可以被当做一个成熟的类型来使用。
协议可以像其他普通类型一样使用,使用场景如下:
注意,协议是一种类型,因此协议类型的名称应与其他类型(例如Int
,Double
,String
)的写法相同,使用大写字母开头的驼峰式写法,例如(FullyNamed
和RandomNumberGenerator
)。
委托(代理)模式
委托是一种设计模式,它允许类或结构体将一些需要它们负责的功能委托给其他类型的实例。委托模式的实现很简单:定义协议来封装那些需要被委托的功能,这样就能确保采纳协议的类型能提供这些功能。委托模式可以用来响应特定的动作,或者接收外部数据源提供的数据,而无需关心外部数据源的类型。
@H_301_81@protocol RandomNumberGenerator { func random() -> Double // 尽管这里并未指明,但是我们假设返回值在[0.0,generator: RandomNumberGenerator) { self.sides = sides self.generator = generator } func roll() -> Int { return Int(generator.random() * Double(sides)) + 1 } } // 两个基于骰子游戏的协议 /** * DiceGame协议可以被任意涉及骰子的游戏采纳。 */ protocol DiceGame { var dice: Dice { get } func play() } /** * DiceGameDelegate协议可以被任意类型采纳,用来追踪DiceGame的游戏过程。 */ protocol DiceGameDelegate { func gameDidStart(game: DiceGame) func game(game: DiceGame,didStartNewTurnWithDiceRoll diceRoll: Int) func gameDidEnd(game: DiceGame) } /** * 蛇梯棋游戏的新版本。新版本使用Dice实例作为骰子,并且实现了DiceGame和DiceGameDelegate协议,后者用来记录游戏的过程。 */ class SnakeAndLadders: DiceGame { let finalSquare = 25 let dice = Dice(sides: 6,generator: LinearCongruentialGenerator()) // dice属性在构造之后就不再改变,且协议只要求dice为只读的,因此将dice声明为常量属性。 var square = 0 var board: [Int] init() { board = [Int](count: finalSquare + 1,repeatedValue: 0) board[03] = +08; board[06] = +11; board[09] = +09; board[10] = +02 board[14] = -10; board[19] = -11; board[22] = -02; board[24] = -08 } var delegate: DiceGameDelegate? // delegate并不是游戏的必备条件,设置为可选 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) } } class DiceGameTracker: DiceGameDelegate { var numberOfTurns = 0 func gameDidStart(game: DiceGame) { // game参数是DiceGame类型而不是SnakeAndLadders类型,所以在方法中只能访问DiceGame协议中的内容。 numberOfTurns = 0 if game is SnakeAndLadders { 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 print("Rolled a \(diceRoll)") } func gameDidEnd(game: DiceGame) { print("The game lasted for \(numberOfTurns) turns") } } let game = SnakeAndLadders() let tracker = DiceGameTracker() 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 */通过扩展添加协议一致性
即便无法修改源代码,依然可以通过扩展令已有类型采纳并符合协议。扩展可以为已有类型添加属性、方法、下标以及构造器,因此可以符合协议中的相应要求。
通过扩展令已有类型采纳并符合协议时,该类型的所有实例也会随之获得协议中定义的各项功能。
通过扩展采纳并符合协议,和在原始定义中采纳并符合协议的效果完全相同。协议名称写在类型名之后,以冒号隔开,然后在扩展的大括号内实现协议要求的内容。
@H_301_81@protocol RandomNumberGenerator { func random() -> Double // 尽管这里并未指明,但是我们假设返回值在[0.0,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) } } /** * 任何想要通过文本表示一些内容的类型都可以实现该协议。这些想要表示的内容可以是实例本身的描述,也可以是实例当前状态的文本描述 */ protocol TextRepresentable { var textualDescription: String { get } } /** * 可以通过扩展,令先前提到的Dice类采纳并符合TextRepresentable协议 */ extension Dice: TextRepresentable { var textualDescription: String { return "A \(sides)-sided dice" } } let d = Dice(sides: 12,generator: LinearCongruentialGenerator()) print(d.textualDescription) // A 12-sided dice extension SnakeAndLadders: TextRepresentable { var textualDescription: String { return "A game of Snakes and Ladders with \(finalSquare) squares" } } let s = SnakeAndLadders() print(s.textualDescription) // A game of Snakes and Ladders with 25 squares通过扩展采纳协议
当一个类型已经符合了某个协议中的所有要求,却还没有声明采纳该协议时,可以通过空扩展体的扩展
来采纳该协议
即使满足了协议的所有要求,类型也不会自动采纳协议,必须显式地采纳协议。
@H_301_81@protocol TextRepresentable { var textualDescription: String { get } } struct Hamster { var name: String var textualDescription: String { return "A hamster named \(name)" } } extension Hamster: TextRepresentable {} // 现在Hamster实例可以作为TextRepresentable类型使用了! let h = Hamster(name: "Tim") let t: TextRepresentable = h print(t.textualDescription) // A hamster named Tim协议类型的集合
协议类型可以在数组或者字典这样的集合中使用。
@H_301_81@protocol RandomNumberGenerator { func random() -> Double // 尽管这里并未指明,但是我们假设返回值在[0.0,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) } } /** * 任何想要通过文本表示一些内容的类型都可以实现该协议。这些想要表示的内容可以是实例本身的描述,也可以是实例当前状态的文本描述 */ protocol TextRepresentable { var textualDescription: String { get } } /** * 可以通过扩展,令先前提到的Dice类采纳并符合TextRepresentable协议 */ extension Dice: TextRepresentable { var textualDescription: String { return "A \(sides)-sided dice" } } extension SnakeAndLadders: TextRepresentable { var textualDescription: String { return "A game of Snakes and Ladders with \(finalSquare) squares" } } struct Hamster { var name: String var textualDescription: String { return "A hamster named \(name)" } } extension Hamster: TextRepresentable {} let s = SnakeAndLadders() let d = Dice(sides: 6,generator: LinearCongruentialGenerator()) let h = Hamster(name: "Tim") let ts: [TextRepresentable] = [s,h,d] // 必须显式声明类型!?否则报出现歧义的错误 for item in ts { print(item.textualDescription) // 任何TextRepresentable的实例都有一个textualDescription属性,所以在每次循环中可以安全地访问textualDescription属性 } /* A game of Snakes and Ladders with 25 squares A hamster named Tim A 6-sided dice */协议的继承
协议能够继承一个或多个其他协议,可以在继承的协议的基础上增加新的要求。协议的继承语法与类的继承相似,多个被继承的协议间用逗号分隔。
@H_301_81@protocol RandomNumberGenerator { func random() -> Double // 尽管这里并未指明,但是我们假设返回值在[0.0,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) } } protocol TextRepresentable { var textualDescription: String { get } } extension SnakeAndLadders: TextRepresentable { var textualDescription: String { return "A game of Snakes and Ladders with \(finalSquare) squares" } } protocol PrettyTextRepresentable: TextRepresentable { var prettyTextualDescription: String { get } } extension SnakeAndLadders: PrettyTextRepresentable { var prettyTextualDescription: String { var output = self.textualDescription + ":\n" for i in 1...finalSquare { switch board[i] { case let ladder where ladder > 0: output += "▲、" case let ladder where ladder < 0: output += "▼、" default: output += "○、" } } return output } } let s = SnakeAndLadders() print(s.prettyTextualDescription) /* A game of Snakes and Ladders with 25 squares: ○、○、▲、○、○、▲、○、○、▲、▲、○、○、○、▼、○、○、○、○、▼、○、○、▼、○、▼、○、 */类类型专属协议
可以在协议的继承列表中,通过添加class
关键字来限制协议只能被类类型采纳,而结构体或枚举不能采纳该协议。class
关键字必须第一个出现在协议的继承列表中,在其他继承的协议之前。
当协议定义的要求需要采纳协议的类型必须是引用语义而非值语义时,应该采用类类型专属协议。
@H_301_81@protocol A { } protocol B: class,A { }协议合成
有时候需要同时采纳多个协议,你可以将多个协议采用protocol<SomeProtocol,AnotherProtocol>
这样的格式进行组合,称为协议合成
(protocol composition)。你可以在<>
中罗列任意多个你想要采纳的协议,以逗号分隔。
协议合成并不会生成新的、永久的协议类型,而是将多个协议中的要求合成到一个只在局部作用域有效的临时协议中。
@H_301_81@protocol Named { var name: String { get } } protocol Aged { var age: Int { get } } struct Person: Named,Aged { var name: String var age: Int } func wishHappyBirthday(celebrator: protocol<Named,Aged>) { // 不关心参数的具体类型,只要参数符合这两个协议即可 print("Happy birthday \(celebrator.name) -- you are \(celebrator.age)") } let p = Person(name: "TIm",age: 23) wishHappyBirthday(p) // Happy birthday TIm -- you are 23检查协议一致性
可以使用类型转换中描述的is
和as
操作符来检查协议一致性,即是否符合某协议,并且可以转换到指定的协议类型。检查和转换到某个协议类型在语法上和类型的检查和转换完全相同:
is
用来检查实例是否符合某个协议,若符合则返回true
,否则返回false
。as?
返回一个可选值,当实例符合某个协议时,返回类型为协议类型的可选值,否则返回nil
。as!
将实例强制向下转换到某个协议类型,如果强转失败,会引发运行时错误。
可选的协议要求
协议可以定义可选要求,采纳协议的类型可以选择是否实现这些要求。在协议中使用optional
关键字作为前缀来定义可选要求
。使用可选要求时(例如,可选的方法或者属性),它们的类型会自动变成可选
的。比如,一个类型为(Int) -> String
的方法会变成((Int) -> String)?
。需要注意的是整个函数类型
是可选的,而不是函数的返回值。
协议中的可选要求可通过可选链式调用
来使用,因为采纳协议的类型可能没有实现这些可选要求。类似someOptionalMethod?(someArgument)
这样,你可以在可选方法名称后加上?
来调用可选方法。
注意,可选的协议要求只能用在标记@objc
特性的协议中。该特性表示协议将暴露给Objective-C
代码,详情参见Using Swift with Cocoa and Objective-C(Swift 2.1)
。即使你不打算和Objective-C
有什么交互,如果你想要指定可选的协议要求,那么还是要为协议加上@obj
特性。
还需要注意的是,标记@objc
特性的协议只能被继承自Objective-C
类的类或者@objc
类采纳,其他类以及结构体和枚举均不能采纳这种协议。
严格来讲,CounterDataSource
协议中的方法和属性都是可选的,因此采纳协议的类可以不实现这些要求,尽管技术上允许这样做,不过最好不要这样写。
协议扩展
协议可以通过扩展来为采纳协议的类型提供属性、方法以及下标的实现。通过这种方式,你可以基于协议本身来实现这些功能,而无需在每个采纳协议的类型中都重复同样的实现,也无需使用全局函数。
@H_301_81@protocol RandomNumberGenerator { func random() -> Double } /** * 一个实现了“线性同余生成器(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) % m return lastRandom / m } } extension RandomNumberGenerator { func randomBool() -> Bool { return random() > 0.5 } } let l = LinearCongruentialGenerator() print(l.random()) // 0.37464991998171 print(l.randomBool()) // true,即使扩展是在LinearCongruentialGenerator定义之后也可以正常调用提供默认实现
可以通过协议扩展来为协议要求的属性、方法以及下标提供默认的实现。如果采纳协议的类型为这些要求提供了自己的实现,那么这些自定义实现将会替代扩展中的默认实现被使用。
注意,通过协议扩展为协议要求提供的默认实现和可选的协议要求不同。虽然在这两种情况下,采纳协议的类型都无需自己实现这些要求,但是通过扩展提供的默认实现可以直接调用,而无需使用可选链式调用。
@H_301_81@protocol TextRepresentable { var textualDescription: String { get } } protocol PrettyTextRepresentable: TextRepresentable { var prettyTextualDescription: String { get } } extension PrettyTextRepresentable { var prettyTextualDescription: String { return textualDescription // 默认的prettyTextualDescription属性,只是简单地返回textualDescription属性的值 } }为协议扩展添加限制条件
在扩展协议的时候,可以指定一些限制条件,只有采纳协议的类型满足这些限制条件时,才能获得协议扩展提供的默认实现。这些限制条件写在协议名之后,使用where
子句来描述。
如果多个协议扩展都为同一个协议要求提供了默认实现,而采纳协议的类型又同时满足这些协议扩展的限制条件,那么将会使用限制条件最多的那个协议扩展提供的默认实现。
@H_301_81@protocol TextRepresentable { var textualDescription: String { get } } struct Hamster { var name: String var textualDescription: String { return "A hamster named \(name)" } } extension Hamster: TextRepresentable {} /** * 扩展CollectionType协议,但是只适用于集合中的元素采纳了TextRepresentable协议的情况 */ extension CollectionType where Generator.Element: TextRepresentable { var textualDescription: String { let itemAsText = self.map { $0.textualDescription } return "[" + itemAsText.joinWithSeparator(",") + "]" } } let tim = Hamster(name: "Tim") let kate = Hamster(name: "Kate") let jack = Hamster(name: "Jack") let hs = [tim,kate,jack] print(hs.textualDescription) // [A hamster named Tim,A hamster named Kate,A hamster named Jack],Array符合CollectionType协议,而数组中的元素又符合TextRepresentable协议