注:本文为自己学习The Swift Programming Language的笔记,其中的例子为引用原书和其他博文或自己原创的。每个例子都会批注一些实践过程中的经验或思考总结。
协议和委托在项目开发中经常使用,因而改变学习顺序先掌握这一部分
1.基础
Swift协议[Protocols]定义了适合某项特殊任务或功能所需要的方法、属性以及其他的要求,相当于Java里的interface,描述了类、结构体或枚举类型所要完成的实现,但它本身不包含任何实现。协议相当于制定了一套规范,满足协议要求称之为遵守[conform]协议。
遵守协议的类型可能要完成特殊的属性、方法、操作符和下标等等。
2.协议语法
协议的定义框架和类、结构体、枚举类似:
protocol FirstProtocol {
//protocal body definition
}
protocol SecondProtocol {
//protocal body definition
}
一个类型可以遵守多个协议,协议在类型名称后面用冒号引出。
如果一个类有父类继承,那么在类名后面首先需要给出父类名,然后再是遵守协议名称:
struct SomeStructure: FirstProtocol, SecondProtocol {
//structure body definition
}
class SomeClass : NSObject, FirstProtocol, SecondProtocol {
//class body definition
}
SomeClass类继承自NSObject,必须写在最前面。
3.协议的属性要求
协议可以要求遵守类型提供有特殊名字和类型的实例属性[instance property]或者类型属性[type property],并且可以规定某个属性具有访问权限[gettable]或者具有访问和设置权限[gettable and settable],但是协议不能指定一个属性是存储属性还是计算属性。具有访问和设置权限的属性不能被[常量存储属性]或者[只读计算属性]实现,而具有访问权限的属性可以被任何形式的属性实现。
属性要求都用var申明为变量,属性类型名后面接{ get set }表示要求属性有访问和设置权限,接{ get }表示属性只有访问权限:
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int{ get }
}
协议要求了一个可读可写的变量属性mustBeSettable和一个只读变量属性doesNotNeedToBeSettable。
如果协议要求属性是类型属性,不管是类实现协议或结构体枚举实现协议,在协议定义时都统一添加class关键字前缀:
protocol AnotherProtocol {
class var someTypeProperty: Int { get set }
}
这个协议在被结构体或枚举实现时仍用static关键字前缀:
struct SomeStruct : AnotherProtocol {
static var someTypeProperty : Int {
get {
return 100
}
set {
}
}
}
下面的例子定义了一个FullyNamed协议,它有一个属性要求:叫做fullname的只读String属性。只读属性可以由任何形式的属性实现,Person结构体中用变量属性实现:
protocol FullyNamed {
var fullName: String { get }
}
struct Person: FullyNamed {
var fullName: String
}
let john = Person(fullName: "John Appleseed")
下面一个复杂的类Starship也遵守FullyNamed协议,飞船名称若有前缀则fullname中包含前缀:
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 ? prefix! + " " : "") + name
}
}
let ncc1701 = Starship(name: "Enterprise", prefix: "USS")
电影星际迷航中代号为NCC-1701的飞船是全名为USS Enterprise[
联邦星舰进取号]的主舰。注意到prefix在构造函数中有初始值,意味着构造函数穿入参数可以省略。
3.协议的方法要求
协议可以要求遵守类型[conforming type]实现特定的实例方法或者类型方法,在协议定义时这些方法的定义和普通方法定义相同,除了省略大括号以及括号内的方法具体实现。参数类型规则和普通方法的参数一样,比如可以是变动参数[variadic parameter],但不允许在协议定义时给参数赋默认值。
关于类型方法的协议要求定义格式和类型属性相同,均使用class关键字作为前缀。
下面的例子中定义一个RandomNumberGenerator随机数生成器协议,它有一个没有穿入参数且返回值类型是Double的方法要求random:
protocol RandomNumberGenerator {
func random() -> Double
}
虽然没有注明,但这个协议期望得到一个0.0到1.0之间的随机数返回值。由于协议并不包含任何实现,所以随机数的生成方法和范围还是由遵守类型决定。
定义一个LinearCongruentialGenerator[线性同余生成器]类遵循随机数生成器协议,它使用一种伪随机数生成算法生成0.0到1.0之间的随机数:
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
}
}
let generator = LinearCongruentialGenerator()
for _ in 1..100 {
println(generator.random())
}
4.协议的突变方法要求
在方法一章中提到值类型[value type]的方法若要改变实例的值那么它必须被声明为突变方法[mutating method],协议通过在方法要求定义中添加mutating关键字声明该方法要求是突变的,这样遵循该协议的结构体或枚举实现该突变方法时可以修改实例的值。类也可以遵循有突变方法要求的协议,只是它实现突变方法时不用添加mutating前缀。
例子中定义一个Togglable[可切换]协议,它有一个突变方法要求以实现“切换”:
protocol Togglable {
mutating func toggle()
}
枚举OnOffSwitch[开关]遵守Togglable协议,它的切换函数实现美剧类型On和Off之间的切换:
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
5.协议作为类型
Swift协议他们自己并不实现任何功能,但是任何创建协议都可以在以下情况中作为一个类型来使用:
(1)函数、方法的传入参数类型或返回值类型,构造函数的穿入参数类型
(2)常量、变量或属性的类型
(3)集合类型如数组、字典等的元素类型
协议作为类型时,它表示一个[遵守该协议的类型]的类型。比如OnOffSwitch枚举是Togglable类型的,LinearCongruentialGenerator类是RandomNumberGenerator类型的,因此协议类型并不是描述的一个协议。由于协议可以作为类型,命名时首字母用大写字母。
下面定义一个Dice[骰子]类,它的实例代表了一个n面骰子,每个骰子都有通过随机数生成器生成的roll[扔骰子]函数模拟扔骰子过程。而这个随机数生成器generator是RandomNumberGenerator类型的,在实例初始化时我们需要传入一个遵守RandomNumberGenerator协议的类型作为generator的类型:
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
}
}
var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator())
for _ in 1...5 {
println("Random dice roll is \(d6.roll())")
}
LinearCongruentialGenerator()创建一个遵守RandomNumberGenerator协议的随机数生成器实例传入给generator,generator可以用random方法产生随机数。
6.委托
委托[delegation]是一种允许类或结构体把自己负责的功能委托给其它类型的实例来实现的设计模式。这种设计模式通过定义一个封装[encapsulate]被委托的功能,使其遵循类型保证能够提供这些被委托的功能。
委托常被用来响应特定的动作,或者从未知类型的外部数据源中获取数据。
用一个比较复杂的骰子游戏来熟悉委托及其用法,首先定义两个协议DiceGame和DiceGameDelegate:
protocol DiceGame {
var dice: Dice { get }
func play()
}
protocol DiceGameDelegate {
func gameDidStart(game: DiceGame)
func game(game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int)
func gameDidEnd(game: DiceGame)
}
DiceGame协议可以被任何有关骰子的游戏遵守,DiceGameDelegate协议用来追踪DiceGame的游戏过程。
Snakes and Ladders是一个简单的骰子游戏,在5*5方块分割的正方形棋盘上每个方块有自己的编号,方块编号从1-25S型排列。有些方块上有梯子,能够通过梯子向终点更进一步;有些方块上有蛇,碰到蛇只能倒退回蛇的尾巴那个方块。梯子和蛇分别用正数和负数表示前进或后退格数,到达终点时获胜。
SnakesAndLadders类遵守DiceGame协议,它有一个属性delegate用来完成委托工作:
class SnakesAndLadders: DiceGame {
let finalSquare = 25
let dice = Dice(sides: 6, generator: LinearCongruentialGenerator())
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?
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)
}
}
仔细分析SnakesAndLadders类,一维数组board模拟棋盘上的蛇和梯子,在构造函数中初始化。可空DiceGameDelegate类型属性delegate用来接收一个委托,这个委托负责游戏的开始、过程和结束,由于其是可空类型没有delegate实例也能完成游戏。while循环中测试每次扔骰子后的当前地点+骰子数值[
square
+ diceRoll],如果刚好到终点就结束;如果超出终点则重新掷骰子;如果没有到终点则向终点前进diceRoll格数并根据board数组实现蛇和梯子的触发效果。
声明一个遵守DiceGameDelegate协议的DiceGameTracker类,它负责追踪游戏进度:
class DiceGameTracker: DiceGameDelegate {
var numberOfTurns = 0
func gameDidStart(game: DiceGame) {
numberOfTurns = 0
if game is SnakesAndLadders {
println("Started a new game of Snakes and Ladders")
}
println("The game is using a \(game.dice.sides)-sided dice")
}
func game(game: DiceGame, didStartNewTurnWithDiceRoll diceRoll: Int) {
++numberOfTurns
println("Rolled a \(diceRoll)")
}
func gameDidEnd(game: DiceGame) {
println("The game lasted for \(numberOfTurns) turns")
}
}
属性numberOfTurns记录游戏结束时话费步数,三个方法打印游戏信息。
完成委托工作的最后一步需要把游戏实例和委托类型的实例建立连接,申请一个tracker实例,将游戏实例game需要委托的工作交付与它:
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
简单的把tracker赋值给game.delegate,使得game.play()方法能够通过委托完成游戏进度追踪工作。
7.使用扩展保持增加协议后的一致性
Swift可以通过扩展[extend]实现一个已有类型采用并遵循一个新的协议,这样不用修改甚至不用知道原类型的源代码就可以完成新协议遵循。扩展[Extension]可以添加新的属性、方法和下标等等,因此总能满足新协议的要求。注意,一旦声明扩展,已存在的该类型的实例也将遵守新的协议。
比如在创建了game实例后,声明一个新协议TextRepresentable,然后让SnakesAndLadders类遵守它,那么已存在的实例game也将遵守新协议:
protocol TextRepresentable {
func asText() -> String
}
extension SnakesAndLadders: TextRepresentable {
func asText() -> String {
return "A game of Snakes and Ladders with \(finalSquare) squares"
}
}
println(game.asText())
// prints "A game of Snakes and Ladders with 25 squares
扩展可以看作类型定义的附加表达,它是“超前”实现的。正是由于扩展的这个性质,我们可以使用扩展来使一个本来实现协议内容但未声明遵守协议的类型显式地表现其遵从协议。
Lannister[兰尼斯特家族—冰与火之歌]类已经实现asText方法,使用空的extension体表明它遵守TextRepresentable协议:
struct Lannister {
var name: String
func asText() -> String {
return "I am \(name) Lannister, who always pays his debts."
}
}
extension Lannister: TextRepresentable {}
extension体内为空,什么也不实现。Lannister类的实例可以被用作
TextRepresentable协议类型实例的赋值:
let kingslayer = Lannister(name: "Jaime")
let somethingTextRepresentable: TextRepresentable = kingslayer
println(somethingTextRepresentable.asText())
// prints I am Jaime Lannister, who always pays his debts.
类型不能通过满足协议的所有要求而自动的采用[adopt]协议,必须显式声明。
8.协议类型的集合
协议类型可以作为集合类型(比如数组和字典)的元素:
let things: TextRepresentable[] = [game, kingslayer]
for thing in things {
println(thing.asText())
}
things是TextRepresentable协议类型的,我们只知道它一定有TextRepresentable协议中的asText方法,而不知道具体它是什么遵从协议类型的实例。
9.协议的继承
一个Swift的协议可以继承其他的一个或几个协议,格式和类继承一样。定义PrettyTextRepresentable继承TextRepresentable协议,它继承asText方法:
protocol PrettyTextRepresentable: TextRepresentable {
func asPrettyText() -> String
}
让SnakesAndLadders类遵守这个新协议,输出详细类型的内容(棋盘信息):
extension SnakesAndLadders: PrettyTextRepresentable {
func asPrettyText() -> String {
var output = asText() + ":\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
}
}
println(game.asPrettyText())
// A game of Snakes and Ladders with 25 squares:
// ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○
10.协议的合成
如果一个类型需要遵守几个协议可以用格式protocol<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(celebrator: protocol<Named, Aged>) {
println("Happy birthday \(celebrator.name) - you're \(celebrator.age)!")
}
let birthdayPerson = Person(name: "Malcolm", age: 21)
wishHappyBirthday(birthdayPerson)
// prints "Happy birthday Malcolm - you're 21!
例子中Person结构体遵从两个协议Named和Aged,函数wishHappyBirthday有一个传入参数,它是Named和Aged合成协议类型的。
协议合成并不是新建了一个新的永久的协议类型,而是暂时的创建了一个囊括几个协议内容的本地的协议。
11.协议的一致性检查
Swift支持使用is和as操作符来检查协议的一致性,并且可以强制转换成另一种协议类型。协议类型检查和转换语法和普通类型一样:
(1)is操作符返回true如果实例遵循协议,否则返回false
(2)as?返回一个可空类型值,如果实例遵从协议则返回协议类型,否则返回nil
(3)as在实例遵从协议时返回协议类型,否则会引发运行时错误
定义一个HasArea协议,只有一个只读属性要求:
@objc protocol HasArea {
var area: Double { get }
}
只有前缀@objc的Swift协议可以被检查一致性,
@objc的协议只能被类遵循,不能被结构体或枚举遵循。@objc协议和Objective-C代码有关。
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 }
}
class Animal {
var legs: Int
init(legs: Int) { self.legs = legs }
}
定义三个类,Circle类和Country类遵循HasArea协议而Animal类不遵循,因为HasArea是@objc前缀的,可以检查关于它的一致性:
let objects: AnyObject[] = [
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]
for object : AnyObject in objects {
if let objectWithArea = object as? HasArea {
println("Area is \(objectWithArea.area)")
} else {
println("Something that doesn't have an area")
}
}
当然由于一致性检查操作符的定义,前两个元素输出面积,而最后一个实例输出“没有面积”。其中AnyObject类型包括所有类型。
12.协议的可选要求
Swift如同Objective-C一样支持协议的可选要求[optional requirement],它表示遵从协议的类型可以选择是否实现该要求,用@optional前缀表示可选要求。
可选要求在调用时需要添加?符号进行检查,例如调用可选方法时格式是:someOptionalMethod?(someArgument)。可选属性在被访问或可选方法返回值时总是返回一个可选类型的值。注意,只有@objc特性的类可以有可选要求,即使不会和Objective-C代码交互操作,这也意味着只有类能遵守有可选要求的协议。
定义一个CounterDataSource[计数器数据源]协议,它有两个可选要求:可选方法incrementForCount用来增加计数、可选只读属性fixedIncrement存储固定增量:
@objc protocol CounterDataSource {
@optional func incrementForCount(count: Int) -> Int
@optional var fixedIncrement: Int { get }
}
定义一个Counter[计数器]类,它的dataSource是CounterDataSource协议类型属性,由于协议的方法要求和属性要求都可空,在调用时使用?操作符检查是否为空。如果dataSource为空或者两个可空要求都为空,那么increment方法不修改count的值:
@objc class Counter {
var count = 0
var dataSource: CounterDataSource?
func increment() {
if let amount = dataSource?.incrementForCount?(count) {
count += amount
} else if let amount = dataSource?.fixedIncrement? {
count += amount
}
}
}
Swift在类名前面加@objc表示这是一个Objective-C类。
这时候可以定义遵循 CounterDataSource协议的数据源类 ThreeSource实现固定增量,这个类并没有实现可选方法incrementForCount:class ThreeSource: CounterDataSource {
let fixedIncrement = 3
}
var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
counter.increment()
println(counter.count)
}
也可以定义实现可选增量方法而没有实现可选属性的类作为dataSource:
class TowardsZeroSource: CounterDataSource {
func incrementForCount(count: Int) -> Int {
if count == 0 {
return 0
} else if count < 0 {
return 1
} else {
return -1
}
}
}
counter.count = -4
counter.dataSource = TowardsZeroSource()
for _ in 1...5 {
counter.increment()
println(counter.count)
}
实现的该方法是让count计数逐渐趋近于0直到等于0。