Swift 协议(Protocols)
2016-06-27 21:42
411 查看
协议定义了一个蓝图,规定了用来实现某一特定工作或者功能所必需的方法和属性。类,结构体或枚举类型都可以遵循协议,并提供具体实现来完成协议定义的方法和功能。任意能够满足协议要求的类型被称为
遵循(conform)这个协议。
协议的语法
协议的定义方式与类,结构体,枚举的定义非常相似。protocol SomeProtocol { // 协议内容 }
要使类遵循某个协议,需要在类型名称后加上协议名称,中间以冒号
:分隔,作为类型定义的一部分。遵循多个协议时,各协议之间用逗号
,分隔。
struct SomeStructure: FirstProtocol, AnotherProtocol { // 结构体内容 }
如果类在遵循协议的同时拥有父类,应该将父类名放在协议名之前,以逗号分隔。
class SomeClass: SomeSuperClass, FirstProtocol, AnotherProtocol { // 类的内容 }
对属性的规定
协议可以规定其遵循者提供特定名称和类型的
实例属性(instance property)或
类属性(type property),而不指定是
存储型属性(stored property)还是
计算型属性(calculate property)。此外还必须指明是只读的还是可读可写的。
如果协议规定属性是可读可写的,那么这个属性不能是常量或只读的计算属性。如果协议只要求属性是只读的(gettable),那个属性不仅可以是只读的,如果你代码需要的话,也可以是可写的。
协议中的通常用var来声明属性,在类型声明后加上
{ set get }来表示属性是可读可写的,只读属性则用
{ get }来表示。
protocol SomeProtocol { var mustBeSettable : Int { get set } var doesNotNeedToBeSettable: Int { get } }
在协议中定义类属性(type property)时,总是使用
static关键字作为前缀。当协议的遵循者是类时,可以使用
class或
static关键字来声明类属性,但是在协议的定义中,仍然要使用
static关键字。
protocol AnotherProtocol { static var someTypeProperty: Int { get set } }
如下所示,这是一个含有一个实例属性要求的协议。
protocol FullyNamed { var fullName: String { get } }
FullyNamed协议除了要求协议的遵循者提供fullName属性外,对协议对遵循者的类型并没有特别的要求。这个协议表示,任何遵循
FullyNamed协议的类型,都具有一个可读的
String类型实例属性
fullName。
下面是一个遵循
FullyNamed协议的简单结构体。
struct Person: FullyNamed{ var fullName: String } let john = Person(fullName: "John Appleseed") //john.fullName 为 "John Appleseed"
这个例子中定义了一个叫做
Person的结构体,用来表示具有名字的人。从第一行代码中可以看出,它遵循了
FullyNamed协议。
Person结构体的每一个实例都有一个叫做
fullName,
String类型的存储型属性。这正好满足了
FullyNamed协议的要求,也就意味着,
Person结构体完整的
遵循了协议。(如果协议要求未被完全满足,在编译时会报错)
下面是一个更为复杂的类,它采用并遵循了
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"
Starship类把
fullName属性实现为只读的计算型属性。每一个
Starship类的实例都有一个名为
name的属性和一个名为
prefix的可选属性。 当
prefix存在时,将
prefix插入到
name之前来为Starship构建
fullName,
prefix不存在时,则将直接用
name构建
fullName。
对方法的规定
协议可以要求其遵循者实现某些指定的实例方法或类方法。这些方法作为协议的一部分,像普通的方法一样放在协议的定义中,但是不需要大括号和方法体。可以在协议中定义具有可变参数的方法,和普通方法的定义方式相同。但是在协议的方法定义中,不支持参数默认值。正如对属性的规定中所说的,在协议中定义类方法的时候,总是使用
static关键字作为前缀。当协议的遵循者是类的时候,虽然你可以在类的实现中使用
class或者
static来实现类方法,但是在协议中声明类方法,仍然要使用
static关键字。
protocol SomeProtocol { static func someTypeMethod() }
下面的例子定义了含有一个实例方法的协议。
protocol RandomNumberGenerator { func random() -> Double }
RandomNumberGenerator协议要求其遵循者必须拥有一个名为
random, 返回值类型为
Double的实例方法。尽管这里并未指明,但是我们假设返回值在[0,1)区间内。
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() print("Here's a random number: \(generator.random())") // 输出 : "Here's a random number: 0.37464991998171" print("And another one: \(generator.random())") // 输出 : "And another one: 0.729023776863283"
对Mutating方法的规定
有时需要在方法中改变它的实例。例如,值类型(结构体,枚举)的实例方法中,将mutating关键字作为函数的前缀,写在
func之前,表示可以在该方法中修改它所属的实例及其实例属性的值。这一过程在在实例方法中修改值类型章节中有详细描述。
如果你在协议中定义了一个方法旨在改变遵循该协议的实例,那么在协议定义时需要在方法前加
mutating关键字。这使得结构和枚举遵循协议并满足此方法要求。
注意:
用类实现协议中的
mutating方法时,不用写
mutating关键字;用结构体,枚举实现协议中的
mutating方法时,必须写
mutating关键字。
如下所示,
Togglable协议含有名为
toggle的实例方法。根据名称推测,
toggle()方法将通过改变实例属性,来切换遵循该协议的实例的状态。
toggle()方法在定义的时候,使用
mutating关键字标记,这表明当它被调用时该方法将会改变协议遵循者实例的状态。
protocol Togglable { mutating func toggle() }
当使用
枚举或
结构体来实现
Togglable协议时,需要提供一个带有
mutating前缀的
toggle方法。
下面定义了一个名为
OnOffSwitch的枚举类型。这个枚举类型在两种状态之间进行切换,用枚举成员
On和
Off表示。枚举类型的
toggle方法被标记为
mutating以满足
Togglable协议的要求。
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 现在的值为 .On
对构造器的规定
协议可以要求它的遵循者实现指定的构造器。你可以像书写普通的构造器那样,在协议的定义里写下构造器的声明,但不需要写花括号和构造器的实体:protocol SomeProtocol { init(someParameter: Int) }
协议构造器规定在类中的实现
你可以在遵循该协议的类中实现构造器,并指定其为类的指定构造器(designated initializer)或者便利构造器(convenience initializer)。在这两种情况下,你都必须给构造器实现标上"required"修饰符:class SomeClass: SomeProtocol { required init(someParameter: Int) { //构造器实现 } }
使用
required修饰符可以保证:所有的遵循该协议的子类,同样能为构造器规定提供一个显式的实现或继承实现。
关于
required构造器的更多内容,请参考必要构造器。
注意
如果类已经被标记为
final,那么不需要在协议构造器的实现中使用
required修饰符。因为final类不能有子类。关于
final修饰符的更多内容,请参见防止重写。
如果一个子类重写了父类的指定构造器,并且该构造器遵循了某个协议的规定,那么该构造器的实现需要被同时标示
required和
override修饰符
protocol SomeProtocol { init() } class SomeSuperClass { init() { // 构造器的实现 } } class SomeSubClass: SomeSuperClass, SomeProtocol { // 因为遵循协议,需要加上"required"; 因为继承自父类,需要加上"override" required override init() { // 构造器实现 } }
可失败构造器的规定
可以通过给协议Protocols中添加可失败构造器来使遵循该协议的类型必须实现该可失败构造器。
如果在协议中定义一个可失败构造器,则在遵顼该协议的类型中必须添加同名同参数的可失败构造器或非可失败构造器。如果在协议中定义一个非可失败构造器,则在遵循该协议的类型中必须添加同名同参数的非可失败构造器或隐式解析类型的可失败构造器(
init!)。
协议类型
尽管协议本身并不实现任何功能,但是协议可以被当做类型来使用。协议可以像其他普通类型一样使用,使用场景:
作为函数、方法或构造器中的参数类型或返回值类型
作为常量、变量或属性的类型
作为数组、字典或其他容器中的元素类型
注意
协议是一种类型,因此协议类型的名称应与其他类型(Int,Double,String)的写法相同,使用大写字母开头的驼峰式写法,例如(
FullyNamed和
RandomNumberGenerator)
如下所示,这个示例中将协议当做类型来使用
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协议的类型的实例都可以赋值给
generator,除此之外,无其他要求。
Dice类中也有一个构造器(initializer),用来进行初始化操作。构造器中含有一个名为
generator,类型为
RandomNumberGenerator的形参。在调用构造方法时创建
Dice的实例时,可以传入任何遵循
RandomNumberGenerator协议的实例给generator。
Dice类也提供了一个名为
roll的实例方法用来模拟骰子的面值。它先使用
generator的
random()方法来创建一个[0,1)区间内的随机数,然后使用这个随机数生成正确的骰子面值。因为generator遵循了
RandomNumberGenerator协议,因而保证了
random方法可以被调用。
下面的例子展示了如何使用
LinearCongruentialGenerator的实例作为随机数生成器创建一个六面骰子:
var d6 = Dice(sides: 6, generator: LinearCongruentialGenerator()) for _ in 1...5 { print("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的游戏过程
如下所示,
SnakesAndLadders是
Snakes and Ladders(Control Flow章节有该游戏的详细介绍)游戏的新版本。新版本使用
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) } }
这个版本的游戏封装到了
SnakesAndLadders类中,该类遵循了
DiceGame协议,并且提供了相应的可读的
dice属性和
play实例方法。(
dice属性在构造之后就不再改变,且协议只要求
dice为只读的,因此将
dice声明为常量属性。)
游戏使用
SnakesAndLadders类的
构造器(initializer)初始化游戏。所有的游戏逻辑被转移到了协议中的
play方法,
play方法使用协议规定的
dice属性提供骰子摇出的值。
注意:
delegate并不是游戏的必备条件,因此
delegate被定义为遵循
DiceGameDelegate协议的可选属性。因为
delegate是可选值,因此在初始化的时候被自动赋值为
nil。随后,可以在游戏中为
delegate设置适当的值。
DicegameDelegate协议提供了三个方法用来追踪游戏过程。被放置于游戏的逻辑中,即
play()方法内。分别在游戏开始时,新一轮开始时,游戏结束时被调用。
因为
delegate是一个遵循
DiceGameDelegate的可选属性,因此在
play()方法中使用了
可选链来调用委托方法。 若
delegate属性为
nil, 则delegate所调用的方法失效,并不会产生错误。若
delegate不为
nil,则方法能够被调用
如下所示,
DiceGameTracker遵循了
DiceGameDelegate协议
class DiceGameTracker: DiceGameDelegate { var numberOfTurns = 0 func gameDidStart(game: DiceGame) { numberOfTurns = 0 if game is SnakesAndLadders { 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") } }
DiceGameTracker实现了
DiceGameDelegate协议规定的三个方法,用来记录游戏已经进行的轮数。 当游戏开始时,
numberOfTurns属性被赋值为0; 在每新一轮中递增; 游戏结束后,输出打印游戏的总轮数。
gameDidStart方法从
game参数获取游戏信息并输出。
game在方法中被当做
DiceGame类型而不是
SnakeAndLadders类型,所以方法中只能访问
DiceGame协议中的成员。当然了,这些方法也可以在类型转换之后调用。在上例代码中,通过
is操作符检查
game是否为
SnakesAndLadders类型的实例,如果是,则打印出相应的内容。
无论当前进行的是何种游戏,
game都遵循
DiceGame协议以确保
game含有
dice属性,因此在
gameDidStart(_:)方法中可以通过传入的
game参数来访问
dice属性,进而打印出
dice的
sides属性的值。
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
在扩展中添加协议成员
即便无法修改源代码,依然可以通过扩展(Extension)来扩充已存在类型(译者注: 类,结构体,枚举等)。扩展可以为已存在的类型添加属性,方法,下标脚本,协议等成员。详情请在扩展章节中查看。注意
通过扩展为已存在的类型遵循协议时,该类型的所有实例也会随之添加协议中的方法
例如
TextRepresentable协议,任何想要表示一些文本内容的类型都可以遵循该协议。这些想要表示的内容可以是类型本身的描述,也可以是当前内容的版本:
protocol TextRepresentable { func asText() -> String }
可以通过扩展,为上一节中提到的
Dice增加类遵循
TextRepresentable协议的功能
extension Dice: TextRepresentable { func asText() -> String { return "A \(sides)-sided dice" } }
现在,通过扩展使得
Dice类型遵循了一个新的协议,这和
Dice类型在定义的时候声明为遵循
TextRepresentable协议的效果相同。在扩展的时候,协议名称写在类型名之后,以冒号隔开,在大括号内写明新添加的协议内容。
现在所有
Dice的实例都遵循了
TextRepresentable协议:
let d12 = Dice(sides: 12,generator: LinearCongruentialGenerator()) print(d12.asText()) // 输出 "A 12-sided dice"
同样
SnakesAndLadders类也可以通过
扩展的方式来遵循
TextRepresentable协议:
extension SnakesAndLadders: TextRepresentable { func asText() -> String { return "A game of Snakes and Ladders with \(finalSquare) squares" } } print(game.asText()) // 输出 "A game of Snakes and Ladders with 25 squares"
通过扩展补充协议声明
当一个类型已经实现了协议中的所有要求,却没有声明为遵循该协议时,可以通过扩展(空的扩展体)来补充协议声明: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 print(somethingTextRepresentable.asText()) // 输出 "A hamster named Simon"
注意
即使满足了协议的所有要求,类型也不会自动转变,因此你必须为它做出显式的协议声明
集合中的协议类型
协议类型可以在集合使用,表示集合中的元素均为协议类型,下面的例子创建了一个类型为TextRepresentable的数组:
let things: [TextRepresentable] = [game,d12,simonTheHamster]
如下所示,
things数组可以被直接遍历,并打印每个元素的文本表示:
for thing in things { print(thing.asText()) } // A game of Snakes and Ladders with 25 squares // A 12-sided dice // A hamster named Simon
thing被当做是
TextRepresentable类型而不是
Dice,
DiceGame,
Hamster等类型。因此能且仅能调用
asText方法
协议的继承
协议能够继承一个或多个其他协议,可以在继承的协议基础上增加新的内容要求。协议的继承语法与类的继承相似,多个被继承的协议间用逗号分隔:protocol InheritingProtocol: SomeProtocol, AnotherProtocol { // 协议定义 }
如下所示,
PrettyTextRepresentable协议继承了
TextRepresentable协议
protocol PrettyTextRepresentable: TextRepresentable { func asPrettyText() -> String }
例子中定义了一个新的协议
PrettyTextRepresentable,它继承自
TextRepresentable协议。任何遵循
PrettyTextRepresentable协议的类型在满足该协议的要求时,也必须满足
TextRepresentable协议的要求。在这个例子中,
PrettyTextRepresentable协议要求其遵循者提供一个返回值为
String类型的
asPrettyText方法。
如下所示,扩展
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 } }
上述扩展使得
SnakesAndLadders遵循了
PrettyTextRepresentable协议,并为每个
SnakesAndLadders类型提供了了协议要求的
asPrettyText()方法。每个
PrettyTextRepresentable类型同时也是
TextRepresentable类型,所以在
asPrettyText的实现中,可以调用
asText()方法。之后在每一行加上换行符,作为输出的开始。然后遍历数组中的元素,输出一个几何图形来表示遍历的结果:
当从数组中取出的元素的值大于0时,用
▲表示
当从数组中取出的元素的值小于0时,用
▼表示
当从数组中取出的元素的值等于0时,用
○表示
任意
SankesAndLadders的实例都可以使用
asPrettyText()方法。
print(game.asPrettyText()) // A game of Snakes and Ladders with 25 squares: // ○ ○ ▲ ○ ○ ▲ ○ ○ ▲ ▲ ○ ○ ○ ▼ ○ ○ ○ ○ ▼ ○ ○ ▼ ○ ▼ ○
类专属协议
你可以在协议的继承列表中,通过添加class关键字,限制协议只能适配到类(class)类型。(结构体或枚举不能遵循该协议)。该
class关键字必须是第一个出现在协议的继承列表中,其后,才是其他继承协议。
protocol SomeClassOnlyProtocol: class, SomeInheritedProtocol { // class-only protocol definition goes here }
在以上例子中,协议
SomeClassOnlyProtocol只能被类(class)类型适配。如果尝试让结构体或枚举类型适配该协议,则会出现编译错误。
注意
当协议想要定义的行为,要求(或假设)它的遵循类型必须是引用语义而非值语义时,应该采用类专属协议。关于引用语义,值语义的更多内容,请查看结构体和枚举是值类型和类是引用类型。
协议合成
有时候需要同时遵循多个协议。你可以将多个协议采用protocol<SomeProtocol, AnotherProtocol>这样的格式进行组合,称为
协议合成(protocol composition)。你可以在
<>中罗列任意多个你想要遵循的协议,以逗号分隔。
下面的例子中,将
Named和
Aged两个协议按照上述的语法组合成一个协议:
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're \(celebrator.age)!") } let birthdayPerson = Person(name: "Malcolm", age: 21) wishHappyBirthday(birthdayPerson) // 输出 "Happy birthday Malcolm - you're 21!
Named协议包含
String类型的
name属性;
Aged协议包含
Int类型的
age属性。
Person结构体
遵循了这两个协议。
wishHappyBirthday函数的形参
celebrator的类型为
protocol<Named,Aged>。可以传入任意
遵循这两个协议的类型的实例。
上面的例子创建了一个名为
birthdayPerson的
Person实例,作为参数传递给了
wishHappyBirthday(_:)函数。因为
Person同时遵循这两个协议,所以这个参数合法,函数将输出生日问候语。
注意
协议合成并不会生成一个新协议类型,而是将多个协议合成为一个临时的协议,超出范围后立即失效。
检验协议的一致性
你可以使用is和
as操作符来检查是否遵循某一协议或强制转化为某一类型。检查和转化的语法和之前相同(详情查看类型转换):
is操作符用来检查实例是否
遵循了某个
协议
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数组使用字面量初始化,数组包含一个
radius为2的
Circle的实例,一个保存了英国面积的
Country实例和一个
legs为4的
Animal实例。
如下所示,
objects数组可以被迭代,对迭代出的每一个元素进行检查,看它是否遵循了
HasArea协议:
for object in objects { 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
当迭代出的元素遵循
HasArea协议时,通过
as?操作符将其
可选绑定(optional binding)到
objectWithArea常量上。
objectWithArea是
HasArea协议类型的实例,因此
area属性是可以被访问和打印的。
objects数组中元素的类型并不会因为强转而丢失类型信息,它们仍然是
Circle,
Country,
Animal类型。然而,当它们被赋值给
objectWithArea常量时,则只被视为
HasArea类型,因此只有
area属性能够被访问。
对可选协议的规定
协议可以含有可选成员,其遵循者可以选择是否实现这些成员。在协议中使用
optional关键字作为前缀来定义可选成员。
可选协议在调用时使用
可选链,因为协议的遵循者可能没有实现可选内容,详细内容在可空链式调用章节中查看。
像
someOptionalMethod?(someArgument)这样,你可以在可选方法名称后加上
?来检查该方法是否被实现。可选方法和可选属性都会返回一个
可选值(optional value),当其不可访问时,
?之后语句不会执行,并整体返回
nil
注意
可选协议只能在含有
@objc前缀的协议中生效。且
@objc的协议只能被
类遵循
这个前缀表示协议将暴露给Objective-C代码,详情参见
Using Swift with Cocoa and Objective-C。即使你不打算和Objective-C有什么交互,如果你想要指明协议包含可选属性,那么还是要加上
@obj前缀
下面的例子定义了一个叫
Counter的整数加法类,它使用外部的数据源来实现每次的增量。数据源是两个可选属性,在
CounterDataSource协议中定义:
@objc protocol CounterDataSource { optional func incrementForCount(count: Int) -> Int optional var fixedIncrement: Int { get } }
CounterDataSource含有
incrementForCount可选方法和
fiexdIncrement可选属性,它们使用了不同的方法来从数据源中获取合适的增量值。
注意
CounterDataSource中的属性和方法都是可选的,因此可以在类中声明都不实现这些成员,尽管技术上允许这样做,不过最好不要这样写。
Counter类含有
CounterDataSource?类型的可选属性
dataSource,如下所示:
@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值作为参数传入。
这里使用了两种可选链方法。由于
dataSource可能为
nil,因此在
dataSource后边加上了
?标记来表明只在
dataSource非空时才去调用
incrementForCount方法。即使
dataSource存在,但是也无法保证其是否实现了
incrementForCount方法,因此在
incrementForCount方法后边也加有
?标记。
调用
incrementForCount方法在上述两种情形都有可能失败,所以返回值为可选
Int类型。虽然在
CounterDataSource中,
incrementForCount被定义为一个非可选
Int(non-optional),但是这里我们仍然需要返回可选
Int类型。
在调用
incrementForCount方法后,
Int型
可选值通过
可选绑定(optional binding)自动拆包并赋值给常量
amount。如果可选值确实包含一个数值,这表示
delegate和方法都存在,之后便将
amount加到
count上,增加操作完成。
如果没有从
incrementForCount(_:)获取到值,可能是
dataSource为nil,或者它并没有实现
incrementForCount方法——那么
increment()方法将试图从数据源的
fixedIncrement属性中获取增量。
fixedIncrement也是一个可选型,所以在属性名的后面添加
?来试图取回可选属性的值。和之前一样,返回值为可选型。
ThreeSource实现了
CounterDataSource协议,它实现来可选属性
fixedIncrement,每次返回值
3:
@objc class ThreeSource: CounterDataSource { let fixedIncrement = 3 }
可以使用
ThreeSource的实例作为
Counter实例的数据源:
var counter = Counter() counter.dataSource = ThreeSource() for _ in 1...4 { counter.increment() print(counter.count) } // 3 // 6 // 9 // 12
上述代码新建了一个
Counter实例;将它的数据源设置为
TreeSource实例;调用
increment()4次。和你预想的一样,每次在调用的时候,
count的值增加3.
下面是一个更为复杂的数据源
TowardsZeroSource,它将使得最后的值变为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,这表示之后不会再有增量。
你可以配合使用
TowardsZeroSource实例和
Counter实例来从
-4增加到
0.一旦增加到
0,数值便不会再有变动。
在下面的例子中,将从
-4增加到
0。一旦结果为
0,便不在增加:
counter.count = -4 counter.dataSource = TowardsZeroSource() for _ in 1...5 { counter.increment() print(counter.count) } // -3 // -2 // -1 // 0 // 0
协议扩展
使用扩展协议的方式可以为遵循者提供方法或属性的实现。通过这种方式,可以让你无需在每个遵循者中都实现一次,无需使用全局函数,你可以通过扩展协议的方式进行定义。例如,可以扩展
RandomNumberGenerator协议,让其提供
randomBool()方法。该方法使用协议中要求的
random()方法来实现:
extension RandomNumberGenerator { func randomBool() -> Bool { return random() > 0.5 } }
通过扩展协议,所有协议的遵循者,在不用任何修改的情况下,都自动得到了这个扩展所增加的方法。
let generator = LinearCongruentialGenerator() print("Here's a random number: \(generator.random())") // 输出 "Here's a random number: 0.37464991998171" print("And here's a random Boolean: \(generator.randomBool())") // 输出 "And here's a random Boolean: true"
提供默认实现
可以通过协议扩展的方式来为协议规定的属性和方法提供默认的实现。如果协议的遵循者对规定的属性和方法提供了自己的实现,那么遵循者提供的实现将被使用。注意
通过扩展协议提供的协议实现和可选协议规定有区别。虽然协议遵循者无需自己实现,通过扩展提供的默认实现,可以不是用可选链调用。
例如,
PrettyTextRepresentable协议,继承了
TextRepresentable协议,可以为其提供一个默认的
asPrettyText()方法来简化返回值
extension PrettyTextRepresentable { func asPrettyText() -> String { return asText() } }
为协议扩展添加限制条件
在扩展协议的时候,可以指定一些限制,只有满足这些限制的协议遵循者,才能获得协议扩展提供的属性和方法。这些限制写在协议名之后,使用where关键字来描述限制情况。(Where语句)。:
例如,你可以扩展
CollectionType协议,但是只适用于元素遵循
TextRepresentable的情况:
extension CollectionType where Generator.Element : TextRepresentable { func asList() -> String { return "(" + ", ".join(map({$0.asText()})) + ")" } }
asList()方法将每个元素以
asText()的方式表示,最后以逗号分隔链接起来。
现在我们来看
Hamster,它遵循
TextRepresentable:
let murrayTheHamster = Hamster(name: "Murray") let morganTheHamster = Hamster(name: "Morgan") let mauriceTheHamster = Hamster(name: "Maurice") let hamsters = [murrayTheHamster, morganTheHamster, mauriceTheHamster]
因为
Array遵循
CollectionType协议,数组的元素又遵循
TextRepresentable协议,所以数组可以使用
asList()方法得到数组内容的文本表示:
print(hamsters.asList()) // 输出 "(A hamster named Murray, A hamster named Morgan, A hamster named Maurice)"
注意
如果有多个协议扩展,而一个协议的遵循者又同时满足它们的限制,那么将会使用所满足限制最多的那个扩展。
相关文章推荐
- 峰回路转,Firefox 浏览器即将重返 iOS 平台
- 峰回路转,Firefox 浏览器即将重返 iOS 平台
- 不可修补的 iOS 漏洞可能导致 iPhone 4s 到 iPhone X 永久越狱
- iOS 12.4 系统遭黑客破解,漏洞危及数百万用户
- Apple Swift学习教程
- 每日安全资讯:NSO,一家专业入侵 iPhone 的神秘公司
- [转][源代码]Comex公布JailbreakMe 3.0源代码
- 介绍 Fedora 上的 Swift
- 讲解iOS开发中基本的定位功能实现
- iOS中定位当前位置坐标及转换为火星坐标的方法
- js判断客户端是iOS还是Android等移动终端的方法
- iOS应用开发中AFNetworking库的常用HTTP操作方法小结
- iOS应用中UISearchDisplayController搜索效果的用法
- iOS App开发中的UISegmentedControl分段组件用法总结
- IOS开发环境windows化攻略
- iOS应用中UITableView左滑自定义选项及批量删除的实现
- iOS中UIAlertView警告框组件的使用教程
- 浅析iOS应用开发中线程间的通信与线程安全问题