协议
一个协议可以定义实现特定任务或者功能篇的的方法、属性或者其他的蓝图。协议不会给出这些要求的具体实现——它只是描述实现后要是说明样子。协议可以被一个类、结构体或者枚举采用,后者需要提供协议要求的实现。任何满足一个协议的类型都可以被称作遵循(译者:conform)了那个协议。
协议可以要求遵循它的类型拥有特定的实例属性、实例方法、类型方法、类型操作符和类型下标。
协议语法
定义一个协议和定义一个类、结构体和枚举很像:
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 } }
协议中,通常用class前缀表示类型属性。这个规则即便是对由结构体或者枚举实现的用static关键字做前缀的类型属性也适用:
protocol AnotherProtocol { class var someTypeProperty: Int { get set } }
这里有一个协议,它有唯一的一个实例属性要求:
protocol FullyNamed { var fullName: String { get } }
FullyNamed协议要求遵循它的类型提供一个全名。它除了要求遵循它的类型要提供一个自己的全名外,没有其他别的要求了。协议规定任何的FullNamed类型必须提供一个只读的、叫做fullName的、String类型的实例属性。
这里是一个遵循FullyNamed协议的简单结构体:
struct Person: FullyNamed { var fullName: String } let john = Person(fullName: "John Appleseed") // john.fullName is "John Appleseed"
这个例子定义了一个叫做Person的结构体,表现了一个有特定名称的人。在它定义的第一行,它遵循了FullyNamed协议。
每个Person实例有一个存储的属性叫做fullName,它是String类型的。这些符合FullyNamed协议的要求,意味着它遵循了那个协议。(如果协议没有被完全满足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 is "USS Enterprise"
这个类用一个只读的计算属性实现了fullName。每个Starship 类实例存储了一个强制的name属性和一个可选的prefiex属性。fullName属性在prefix值存在的情况下会使用它的值,使用时会将它的值放置在name之前。
方法的要求
协议可以要求遵循它的类型有指定的实例方法或和类型方法。这些方法作为协议定义的一部分,写法和普通的实例方法和类型方法一样,区别是不需要有大括号和方法体。可变参数是被允许的,和普通的方法一样。
NOTE
协议中的方法语法和普通方法的语法一样,除了不允许指定方法参数的默认值之外。
和类型属性的要求一样,使用class关键字做前缀在一个协议内定义类型方法。即便是协议最终由结构体或者枚举实现也要这样做:
protocol SomeProtocol { class 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) % m) return lastRandom / m } } let generator = LinearCongruentialGenerator() println("Here's a random number: \(generator.random())") // prints "Here's a random number: 0.37464991998171" println("And another one: \(generator.random())") // prints "And another one: 0.729023776863283"
变异方法的要求
有时需要一个方法修改它所在的实例。对于在值类型(就是结构体和枚举)中的实例方法可以在方法的func关键字之前放置mutating关键字表明那个方法可以修改它所在的实例、同时/或者实例的任意属性。这在 用实例方法修改值类型(Modifying Value Types from Within Instance Methods)有描述。
如果一个协议定义了实例方法的要求,要求中想要修改任意遵循该协议的类型的实例,那么用mutating关键字标记方法吧。这样使得遵循这样的协议的结构体和枚举满足方法的要求。
NOTE
如果将一个协议的实例方法要求标记为了mutating,在遵循了这个协议的类中不需要再写mutating关键字了,mutating关键字只在遵循这个协议的结构体和枚举中使用。
下面的例子定义了一个叫做Togglable的协议,它定义了唯一的一个实例方法需求,叫做toggle。就像它的名字暗示的一样,toggle方法将要切换遵循那个协议的类型的状态,通常是通过修改该类型的属性(实现)。
作为Toggleable协议定义的一部分,toggle方法被用mutating关键字标记了,这表明这个方法在调用时会修改遵循这个协议的实例:
protocol Togglable { mutating func toggle() }
如果用结构体或者枚举实现这个Togglable协议,结构体或者枚举要提供一个同样用mutating标记的toggle方法。
下面的例子定义了一个叫做onOffSwitch的枚举。这个枚举在两个状态之间来回切换,通过枚举的两个case:On和Off表示。枚举的toggle实现被mutating标记了,为了满足Toggleable协议的要求:
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 } }
使用required修饰符可以明确的对遵循协议的类的子类进行要求,这样那些子类也要遵循协议。
更多的关于 必要构造方法的内容 参见 必要构造方法(required Initializers)。
NOTE
不需要对实现协议构造方法的、标记为final的类的构造方法实现标记required,因为最终类不会有子类了。更多的关于final修饰符的信息,参见 阻止重写(Preventing Overrides)。
如果子类重写了一个超类的指定构造方法,同时这个构造方法实现了一个协议的构造方法要求,需要同时采用requried和override修饰符对这个构造方法实现进行标记:
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 } }
可失败构造方法的要求
协议可以给实现类型定义一个可失败的构造方法要求,就像 可失败的构造方法(Failable Initializer)中的一样。
一个可以失败构造方法的要求可以由实现类型的可失败构造方法或者不可失败构造方法来实现。一个不可失败的构造方法的需求可以由一个可失败的构造方法或者静默拆包可失败构造方法实现。
协议作为类型
协议本身不会实现任何功能。但是,协议可以作为类型使用。
因为协议是类型,可以在一些其他类型适用的场合使用协议,包括:
函数、方法或者构造方法的参数类型或者返回类型
常量、变量或者属性的类型
作为数组、字典或者其他容器的类型
NOTE
因为协议是类型,它们的名字首字母需要大写(比如FullNamed和RandomNumberGenerator),这样就和Swift的其他的类型一致了(比如,Int,String,Double)。
这里有一个协议被当作类型使用的例子:
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 } }
这个例子定义了一个新的类叫做Dice,它表现了游戏中使用的一个有N个面的色子。Dice实例有一个整型的属性叫做sides,这个属性表示色子有多少个面;还有一个叫做generator的属性,这个属性为摇色子提供了一个随机数字生成器。
generator属性是RandomNumberGenerator类型的。因此可以给它赋值任何类型,只要这个类型遵循了RandomNumberGenerator 协议。除此之外没有其他任何的要求了。
Dice用一个构造方法设置它的出事状态。这个构造方法有一个叫做generator的参数,这个参数是RandomNumberGenerator 类型的。当需要初始化一个新的Dice实例时,传递一个遵循RandomNumberGenerator 协议的值就可以了。
Dice提供了一个实例方法:roll,这个方法返回一个从1到色子面数之间的整数。这个调用了generator属性的random方法创建了一个从0.0到1.0的随机数,然后使用这个随机数创建出摇色子的数字。因为gnerator是遵循RandomNumberGenerator协议的,所以一定可以调用它的random 方法。
下面的例子是如何使用Dice类,通过采用一个LinearCongruentialGenerator 实例做随机数字产生器,创建出一个六面的色子:
var d6 = Dice(sides: 6,generator: LinearCongruentialGenerator()) for _ in 1...5 { println("Random dice roll is \(d6.roll())") } // Random dice roll is 3 // Random dice roll is 5 // Random dice roll is 4 // Random dice roll is 5 // Random dice roll is 4
委托
委托是一种设计模式:一个类或者结构体将它自身的职责推开(或者委托)给其他类型的的一个实例。这种设计模式用一个协议来实现,协议包含了要委托的职责,这样,遵循协议的类型(就是委派代表)一定提供了被委托的功能。委托可以用来应对特定行为,或者从外部的源读取数据而不需要事先知道外部源的类型。
下面的例子为上面的摇色子游戏定义了两个协议:
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的类型遵循。
这里有一个在 控制流(Control Flow)中介绍的 蛇和梯子(Snakes and Ladders)游戏的新版本。这个版本使用了Dice实例;遵循了DiceGame协议;游戏过程中通知DiceGameDelegate :
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) } }
关于 蛇和梯子游戏的玩法,参见控制流一章的Break一节。
这个版本的游戏用一个叫做SnakeAndLadders的类包装,它遵循DiceGame协议。为了满足协议要求它提供一个只读的dice属性和一个paly方法。(dice属性被声明为常量,因为在初始化过后就不需要修改它了,而且协议只要求这个属性可读就行了)
游戏的初始化在这个类的构造方法init()中进行。所有的游戏逻辑挪到了协议的play方法中,方法中使用了协议中要求的dice属性,用来提供摇色子的值。
这里delegate属性被声明为了一个可选的DiceGameDelegate,因为一个委托代表不是必须的,所以它是一个可选类型,正因为如此,delegate属性被自动设置一个初始值nil。同时,游戏实例可以选择设置一个适合的委托代表给这个属性。
DiceGameDelegate 提供了三个方法来监控游戏的过程。三个方法在上面提到的play方法中,分别在游戏开始、游戏结束、游戏新一轮开始的时候被调用,已经和逻辑混合在一起了。
因为delegate 属性是一个可选DiceGameDelegate类型,paly方法每次调用委托代表上的方法时采用了可选类型链。如果delegate 属性是nil,这个委托代表调用会优雅的失败而不会产生错误。如果delegate 属性不是nil,委托代表的方法就被调用,SnakesAndLadders 被当成参数传递给该方法。
下面展示了一个叫做DiceGameTracker的类,这个类遵循DiceGameDelegate 协议:
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") } }
DiceGameTracker 实现了DiceGameDelegate协议要求的全部三个方法。用这些方法,这个类监控了游戏进行了多少轮(译者:存储在numberOfTurns变量中)。当游戏开始时,将numberOfTurns的数字清零;每轮开始时对numberOfTurns做自加操作;在游戏结束时,将numberOfTurns的数字打印出来。
如上所示的gameDidStart 实现,使用game参数打印了关于将要进行的游戏的引导信息。game参数是DiceGame类型的而不是SnakesAndLadders,所以gameDidStart方法只能访问和使用作为DiceGame协议实现的那部分属性和方法。不管怎样,gameDidStart 方法都可以利用造型查询一个实例的具体类型。这个例子中,它就检查了game是不是一个SnakesAndLadders 类的实例,如果属实,还打印了对应的信息。
gameDidStart方法还访问了传递来的game参数的dice属性。因为game是遵循DiceGame协议的,所以它肯定会有一个dice属性,所以gameDidStart 方法能够方法和打印色子的sides属性,不必理会game具体的类型。
下面是DiceGameTracker 如何工作的情况:
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
用扩展实现协议
尽管不能访问已经存在类型的源代码,但可以扩展一个已经存在的类型,使其遵循一个新的协议。扩展可以给已经存在的类添加新的属性、方法和下标,因此可以添加协议的任何要求。关于扩展更多的信息,参见 扩展(Extensions)。
NOTE
当用扩展给类型添加了协议的要求后,这些类型的已经存在的实例就都遵循那个协议了。
举例说明,这个叫做TextRepresentable的协议,可以被任何可以用文本表示的类型实现。这个文本可能是自我描述,或者是当前状态的文本:
protocol TextRepresentable { func asText() -> String }
前面提到的Dice类可以通过扩展遵循TextRepresentable协议:
extension Dice: TextRepresentable { func asText() -> String { return "A \(sides)-sided dice" } }
这个扩展使Dice遵循了一个新的协议就像在其原始实现中提供了一样。协议的名字在类型之后提供,用冒号分割,在扩展内容的大括号中提供协议所有要求的实现。
现在,任何一个Dice实例都可以被当作TextRepresentable对待了:
let d12 = Dice(sides: 12,generator: LinearCongruentialGenerator()) println(d12.asText()) // prints "A 12-sided dice"
类似的,SnakesAndLadders 也可以通过扩展遵循TextRepresentable 协议:
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"
通过扩展宣布遵循一个协议(Declaring Protocol Adoption with an Extension)
如果一个类型已经满足一个协议的全部要求,但是没有规定遵循那个协议,可以通过一个空的扩展使类型遵循协议:
struct Hamster { var name: String func asText() -> String { return "A hamster named \(name)" } } extension Hamster: TextRepresentable {}
Hamster的实例可以被当作TextRepresentable 类型处理:
let simonTheHamster = Hamster(name: "Simon") let somethingTextRepresentable: TextRepresentable = simonTheHamster println(somethingTextRepresentable.asText()) // prints "A hamster named Simon"
NOTE
类型并不会通过满足协议的要求自动就遵循协议。需要明确的声明才可以。
协议类型的集合(Collections of Protocol Types)
就像 协议作为类型(Protocols as Types)提到的,协议可以作为集合(比如数组、字典)的类型使用。这个例子创建了一个TextRepresentable 类型的数组:
let things: [TextRepresentable] = [game,d12,simonTheHamster]
现在迭代数组中的每一项,打印每一项的文本表示:
for thing in things { println(thing.asText()) } // A game of Snakes and Ladders with 25 squares // A 12-sided dice // A hamster named Simon
tings的内容是TextRepresentable。而不是Dice、DiceGame或者Hamster类型的,尽管实际上它们是这些类型的。因为是TextRepresentable类型的,而TextRepresentable类型已知只有asText方法,所以在循环中调用thing.asText是安全的。
协议的继承(Protocol Inheritance)
一个协议可以继承一个或多个其他的协议,同时添加进一步的要求。协议继承的语法和类继承的语法类似,不同的是可以继承多个协议,用逗号分隔他们:
protocol InheritingProtocol: SomeProtocol,AnotherProtocol { // protocol definition goes here }
这里是一个继承了前文TextRepresentable 协议的一个协议:
protocol PrettyTextRepresentable: TextRepresentable { func asPrettyText() -> String }
这个例子定义了一个新的协议:PrettyTextRepresentable,它继承了TextRepresentable协议。任何遵循PrettyTextRepresentable 协议的类型必须满足TextRepresentable协议的所有要求,另外加上PrettyTextRepresentable协议的。在这个例子中,PrettyTextRepresentable 协议添加了要求,需要提供一个叫做asPrettyText的实例方法,该方法返回一个String。
SnakesAndLadders 类可以通过扩展实现PrettyTextRepresentable:
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 } }
这个扩展规定要遵循PrettyTextRepresentable 协议,而且提供了SnakesAndLadders 类型的asPrettyText 方法实现。任何是PrettyTextRepresentable 类型的同时也是TextRepresentable类型的,所以asPrettyText 实现中以调用来自于TextRepresentable 协议的asText 方法开始,给output一个初始值。初始值后之后添加一个冒号和一个换行符,这些一起作为漂亮文字表述(pretty text representation)的开始。接下来遍历游戏格子的数组,用一个几何图形表示每个格子:
如果格子的值大于0,那么它是一个梯子,用▲表示。
如果格子的值小于0,那么它是条蛇,用▼表示。
不是以上情况,那么它是空格子,用○表示。
这个方法就可以被用来打印任何SnakesAndLadders 类型的实例的漂亮文字表述了:
println(game.asPrettyText()) // A game of Snakes and Ladders with 25 squares: // ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○
只对类开放的协议(Class-Only Protocols)
在协议的继承列表中,可以用class关键字限制协议只对类开放。class关键字通常放置在协议继承列表的最开始,在任何继承的协议名字之前:
protocol SomeClassOnlyProtocol: class,SomeInheritedProtocol { // class-only protocol definition goes here }
上面的例子中,SomeClassOnlyProtocol 只能被类实现。尝试让结构体或者枚举遵循SomeClassOnlyProtocol,会导致编译时错误。
NOTE
当协议定义的行为和引用相关而不是和值相关时,考虑采用只对类开放的协议(Class-Only Protocols)。关于引用和值的相关内容参见 结构体和枚举是值类型的 (Structures and Enumerations Are Value Type)和 类是引用类型的(Class Are Reference Type)
协议组合(Protocol Composition)
需要让一个类型同时遵循多个协议,这是很正常的。采用一个协议组合,可以将多个协议组合为一个(协议)需求。协议组合以这样的形式出现:protocol
是否满足协议的检查(Checking for Protocol Conformance)
可以使用造型 (Type Casting)中描述的is 和 as操作符检查是否满足协议和造型为特定协议。下面的对协议的检查和造型的语法和对类型的检查和造型的语法一样:
如果实例遵循一个协议,is操作法返回ture,否则返回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数组采用了字面初始化,其中包含一个Circle实例(半径为2);一个Country实例(用英国的国土面积初始化);和一个有四条腿的Animal实例。
objects数组可以被迭代,其中的每个对象可以被检查是否遵循了HasArea协议:
for object in objects { if let objectWithArea = object as? HasArea { println("Area is \(objectWithArea.area)") } else { println("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属性的访问和打印是类型安全的。
注意,潜在的对象没有通过造型处理被改变。它们任然是Circle、Country和Animal。但是,一旦它们被存储在objectWithArea常量中,它们就被当作HasArea类型,所以它们的area属性可以被访问。
@H_404_327@可选协议要求(Optional Protocol Requirements)可以给些一定义可选要求,这些要求不必一定被遵循的类型实现。协议定义中用前缀optional标记可选要求。
一个可选协议要求可以用可选类型链调用,提供了这些要求可以不被遵循协议的类型是实现的可能。更多关于可选类型链的信息,参见 可选类型链(Optional Chainning)。
可以在名字后面加一个问号的方式检查一个可选要求是否被实现,比如someOptionalMethod?(someArgument)。可选属性要求、和返回一个值的可选方法要求 将会始终返回一个对应的可选类型,当它们被访问或者被调用时,这将反映可选要求是不是被实现了。
NOTE
可选类型要求只能在用@objc标记的协议中定义。尽管你可能不需要和OC交互,但你需要这个标记定义可选要求。
同时要注意@objc标记的协议只能被类实现,结构体和枚举不行。
下面的类型定义了一个给整型计数的类,叫做Counter,这个类使用外部数据源给自身增加数量。这里的外部数据源采用CounterDataSource协议定义,其中有两个可选的要求:
@objc protocol CounterDataSource { optional func incrementForCount(count: Int) -> Int optional var fixedIncrement: Int { get } }
CounterDataSource 协议定义了一个可选的方法要求,叫做incrementForCount ;还有一个可选的属性要求,叫做fixedIncrement。这些要求定义了两种不同的给Counter实例添加数量的方法。
NOTE
坦白说,可以定义一个类遵循CounterDataSource 协议,但不实现任何要求。这些要求都是可选的。尽管从技术角度这是允许的,但会产生一个没有实际的意义的数据源。
下面定义的Counter类,有一个可选的dataSource属性,它是CounterDataSource?类型的:
@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 } } }
Counter类存储了它的当前的值在一个变量属性count中。同时它还定义了一个叫做increment的方法,每次这个方法被调用时,count属性会自增。
increment方法开始尝试查看它的数据源的incrementForCount 的实现来增加数值。increment方法是用了可选类型链,尝试调用incrementForCount,还将当前的count值传递给了incrementForCount方法做参数。
这里用了两级可选类型链。首先dataSource可能是nil,所以datasource名字后面有一个问号表示,只有dataSource不是nil的时候才调用incrementForCount方法。接下来,如果dataSource存在,但不确定它一定实现了incrementForCount方法,因为这个方法是一个可选要求。这就是为什么incrementForCount的名字后后面为什么也会有一个问号。
上面的俩个原因之一,都回导致调用incrementForCount方法失败,所以这个调用返回一个可选Int值。在CounterDataSource中定义了incrementForCount 方法时,这个语句才会有效。
在调用了incrementForCount方法之后,通过可选绑定,可选Int拆包后会赋值给一个常量amount。如果可选Int有值,就意味着方法和方法的委托都存在,同时方法还返回一个值,拆包后的amount 常量被添加到了存储属性count上面,增加数量的操作就完成了。
如果dataSource是nil,或如果数据源没有实现incrementForCount,都会造成不能从incrementForCount 方法得到值,如果是这样的情况,increment 方法就会尝试从数据源的fixedIncrement 属性中得到值。fixedIncrement 属性同样是一个可选要求,所以它的名字后面也用可选类型链也就是一个问号,这就表明尝试访问属性的值可能会失败。同前所述,这样返回的值也是一个可选Int的值,尽管在CounterDataSource 协议的定义中,fixedIncrement 被定义为一个不可选的Int属性。
这里是一个简单的CounterDataSource 实现,每次被请求的时候数据源都回返回一个常量值3.这些都是通过实现可选的fixedIncrement属性要求实现的:
class ThreeSource: CounterDataSource { let fixedIncrement = 3 } 可以用ThreeSource 的一个实例作为一个新的Counter实例的数据源: var counter = Counter() counter.dataSource = ThreeSource() for _ in 1...4 { counter.increment() println(counter.count) } // 3 // 6 // 9 // 12
上面的代码创建了一个新的Counter实例;将一个新的ThreeSource 实例的赋值给了这个Counter实例的数据源;然后调用了counter的increment方法四次。和预期的一样,counter的count属性在increment方法被调用时会被加上3.
这里有一个更复杂的叫做TowardsZeroSource的数据源,它会使得Counter实例的count值趋于0:
class TowardsZeroSource: CounterDataSource { func incrementForCount(count: Int) -> Int { if count == 0 { return 0 } else if count < 0 { return 1 } else { return -1 } } }
TowardsZeroSource 类实现了CounterDataSource 协议中的可选incrementForCount 方法,根据count参数判断应该朝哪个方向计数。如果count已经是0了,那么方法就会返回0,不需要再进行计数下去了。
可以对一个已经存在的Counter实例使用一个TowardsZeroSource 实例,从-4计数到0,到0后就不再计数了:
counter.count = -4 counter.dataSource = TowardsZeroSource() for _ in 1...5 { counter.increment() println(counter.count) } // -3 // -2 // -1 // 0 // 0