终于到了最让人激动的特性了, 拓展和协议是Swift里面的一大亮点, 可以说会改变目前我们写代码的一些方式(思维还是一致的). 虽然很重要且给力, 但是内容还是不难的, 只需要记住一些概念即可.
拓展
拓展其实和ObjC里面的category很想(貌似ObjC里面的匿名category也叫拓展, 但是感觉功能比较弱). Swift里面的拓展被增强了很多, 可以扩展类, 结构体, 枚举,甚至是协议, 可以做这么些事情:
1). 增加运算属性和类运算属性(只能是运算属性, 不能是存储属性, 因为, 如果增加存储属性, 原来的类的构造器是没有初始化它们的.)
2). 定义实例方法和类方法
3). 提供新的构造器(只能是便利构造器)
4). 定义下标
5). 定义和使用聚合类型
6). 使已有的类型实现协议
可以看到, 原生类里能做的大多数事情, 拓展里面都可以做, 是一种增强原始类能力的一大利器. 更有意思的是, extension还能扩展协议, 提供协议的实现, 这个具体在协议部分再讲. 总之, 扩展是Swift提供的一个强大的功能, 可以改变我们目前很多的代码结构. 例如, 看了objc.io里面的第一章, 更轻量的viewcontrollerss就会知道, 在ObjC中, viewcontroller容易被写的很重, 很多的UI, 业务逻辑, 响应逻辑被堆在viewcontroller中, 所以一般我们都会拆分掉, 比如最tableview的datasource会另写一个类来实现. 但是有了extension, 我们直接扩展viewcontroller就好了, 让扩展部分来承载datasource的实现.
需要注意的是, 拓展可以新增功能, 但是不能重载功能. 同时, 一旦定义了拓展, 那么这些拓展的功能将对所有的该类型实例生效, 即使创建在定义拓展之前的.
扩展的语法
直接看例子吧:
// 扩展类型:
extension SomeType {
// new functionality to add to SomeType goes here
}
// 实现协议:
extension SomeType: SomeProtocol, AnotherProtocol {
// implementation of protocol requirements goes here
}
运算属性
直接看官方给出对Double的运算属性拓展, 给Double加上了各种长度单位:
extension Double {
var km: Double { return self * 1_000.0 }
var m: Double { return self }
var cm: Double { return self / 100.0 }
var mm: Double { return self / 1_000.0 }
var ft: Double { return self / 3.28084 }
}
let oneInch = 25.4.mm
print("One inch is \(oneInch) meters")
// prints "One inch is 0.0254 meters"
let threeFeet = 3.ft
print("Three feet is \(threeFeet) meters")
// prints "Three feet is 0.914399970739201 meters"
let aMarathon = 42.km + 195.m
print("A marathon is \(aMarathon) meters long")
// prints "A marathon is 42195.0 meters long"
构造器
开篇提到了extension只能拓展便利构造器, 这是因为, 在构造器里面, 我们知道继承构造器的条件, 其中第二个条件是"子类提供了全部的父类指派构造器而不是从情况1获得的, 即使是提供了一部分实现, 那么它将自动继承所有的父类便利构造器", 如果extension拓展了指派构造器, 那么该类的继承者就无法继承便利构造器了, 之前调用的地方自然就都出错了.
值得一说的是, 析构器也只能在原始类中定义和实现, 毕竟, extension不能有存储属性, 也就没什么需要释放资源的情况了.
提醒一下, 之前也在构造器那章说过, 如果值类型为每个属性都提供了默认值并且没有实现任何自定义的构造器, Swift会为之生成默认构造器. 如果我们在extension里面新增了一个构造器, Swift不会把默认构造器去掉(不然以前调用的地方不就出错了么...)
看一个例子来吧:
struct Size {
var width = 0.0, height = 0.0
}
struct Point {
var x = 0.0, y = 0.0
}
struct Rect {
var origin = Point()
var size = Size()
}
// 使用之:
let defaultRect = Rect()
let memberwiseRect = Rect(origin: Point(x: 2.0, y: 2.0),
size: Size(width: 5.0, height: 5.0))
// 拓展构造器
extension Rect {
init(center: Point, size: Size) {
let originX = center.x - (size.width / 2)
let originY = center.y - (size.height / 2)
self.init(origin: Point(x: originX, y: originY), size: size)
}
}
// 使用新增的构造器
let centerRect = Rect(center: Point(x: 4.0, y: 4.0),
size: Size(width: 3.0, height: 3.0))
方法
直接根据例子来讲吧, 比如官方拓展了Int, 让Int可以重复执行闭包:
extension Int {
func repetitions(task: () -> Void) {
for _ in 0..<self {
task()
}
}
}
// 打印3次Hello
3.repetitions({
print("Hello!")
})
// 之前提过的简化写法:
3.repetitions {
print("Goodbye!")
}
拓展增加的方法也是可以修改实例本身或者一些属性的, 当然, 改变值类型属性的函数还是要加mutating的. 例如:
extension Int {
mutating func square() {
self = self * self
}
}
var someInt = 3
someInt.square() // someInt为9
下标
一开始也说了可以在extension中拓展下标, 但是还是要记住不能重载已有的下标运算符. 直接看例子:
// 这个奇怪的例子返回的是Int型数据中, 从右往左数的第几个数字是几
extension Int {
subscript(var digitIndex: Int) -> Int {
var decimalBase = 1
while digitIndex > 0 {
decimalBase *= 10
--digitIndex
}
return (self / decimalBase) % 10
}
}
746381295[0]
// returns 5
746381295[1]
// returns 9
746381295[2]
// returns 2
746381295[8]
// returns 7
聚合类型
还是直接看例子吧, 并没有太多新的东西, 或者直接跳过不看也可以:
extension Int {
enum Kind {
case Negative, Zero, Positive
}
var kind: Kind {
switch self {
case 0:
return .Zero
case let x where x > 0:
return .Positive
default:
return .Negative
}
}
}
// 使用
func printIntegerKinds(numbers: [Int]) {
for number in numbers {
switch number.kind {
case .Negative:
print("- ", terminator: "")
case .Zero:
print("0 ", terminator: "")
case .Positive:
print("+ ", terminator: "")
}
}
print("")
}
printIntegerKinds([3, 19, -27, 0, -6, 0, 7])
打印出 + + - 0 - 0 +
很有用的一章, 而且知识点也不难, 具体细节可以参考官方文档
协议
协议在Objc里面也有, 一般会被类比为Java里的接口或者C++中的纯虚类, 不管是协议还是接口, 其本质都是定义你要被外界所影响的方式.
Swift2.0发布后, Swift的编程的主要思想(或者说范式)就转向了面向协议编程(不仅仅只有面向协议). 面向对象(OOP)和面向协议(POP)的编程思想最大的区别就如其名所描述, 前者把世界万物都看做一个个对象, 后者则不一样, 不在乎世界万物的本质, 只在乎你是否能做某些事件. 举个例子来说明其中的区别吧:
我们想让所有的类都实现某个方法, 如description, 打印出可以描述当前对象的信息, 如果是
OOP, 我们很自然的想到写一个基类, 提供默认实现的description方法, 正如NSObject所做的一样, 但是POP则会先声明一个Printable的协议, 然后用extension来提供默认实现(Swift2.0之后才可以对协议extension, 所以才说2.0之后才是POP), 之后每一个类,结构体或者枚举, 只要声明实现了这个协议, 就可以使用这个方法了. 这就是二者的区别, 所以, 在切换到Swift之后, 我们的编程思想也要发生相应的变化, 而不是沿袭之前面向对象的思考方式.
上面的话都说明了协议在Swift里的重要性, 里面包含的细节比较多, 官方文档给出的也都是相对基础的内容, 有机会的话会尝试更多的有关于协议的内容, 然后分享出来.
先进入协议的世界吧.
协议语法
protocol SomeProtocol {
// protocol definition goes here
}
// 协议可以被类,结构体和枚举实现
struct SomeStructure: FirstProtocol, AnotherProtocol {
// structure definition goes here
}
class SomeClass: SomeSuperclass, FirstProtocol, AnotherProtocol {
// class definition goes here
}
// 但是如果这么写就只能被类实现了, 没有只能被struct和enum实现的协议
protocol ClassOnlyProtocol : class{ // <-- 加上: class
// class only protocol definition goes here
}
需要提一句的是, Objc的协议是弱要求, 不实现只会报警, 但是Swift里面是强要求, 不实现直接编译不通过.
属性和方法要求
协议可以要求其实现者拥有某些属性和方法, 还可以定制要求其提供get和set方法, 如:
protocol SomeProtocol {
var mustBeSettable: Int { get set }
var doesNotNeedToBeSettable: Int { get }
static var someTypeProperty: Int { get set } // <-- 类属性
}
protocol RandomNumberGenerator {
func random() -> Double
}
可变方法要求
因为协议既可以被类实现, 又可以被结构体和枚举实现, 所以如果要求实现的某些方法里面修改了结构体或枚举自身或者属性的值(参考之前的笔记五), 那么就需要加上mutating(如果不想结构体继承, 就显式如协议语法小节所说加上: class).
如果协议没有加上mutating但是实现的结构体或者枚举自行加上, 则会报错, 认为没有实现, 同时, 如果自己再加上一个不加mutating的方法, 则认为重复命名方法了, 个人感觉这里处理的相当矛盾...
总之语法如此, 所以在写protocol方法的时候, 如果要给结构体和枚举用, 就在方法前加上mutating
构造器要求
和方法一样, 协议中也可以要求实现者实现特定的构造器. 如:
protocol SomeProtocol {
init(someParameter: Int)
}
// 实现者可以把这个构造器实现为指定构造器或者便利构造器, 但是必须要加上required修饰符
class SomeClass: SomeProtocol {
required init(someParameter: Int) {
// initializer implementation goes here
}
}
// 用required修饰符后, 其子类也必须要有这么一个构造器(如果本类用final修饰之后, 就不需要用required来修饰了, 因为这个类已经不能被继承了), 参考笔记七--构造器;
// 如果子类实现协议指定的构造器与父类重名了, 那么required和override都必须写上.
// 进一步来看, 协议也可以要求实现可失败的构造器, 同样具体参考笔记七.
把协议当做类型
这个概念在Swift里面比较重要, 后面的几个小节都可以基于这一点来理解. 与ObjC不一样, 协议并不能独立成为一个类型, 一般都会和id合用, 例如: id<UITableViewDataSource>
协议在很多场合下和类型是差不多的, 例如:
1). 在函数, 方法或者构造器中被当做参数类型, 返回值类型
2). 用作常量, 变量或者属性的类型
3). 用作数组, 字典或者其它容器内部对象的类型
正因为如此, 所以协议命名的时候也要求以大写开头.
看看官方的例子:
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 {
print("Random dice roll is \(d6.roll())")
}
// 这里再次强调, 面向协议不在乎你是什么, 只在乎你能做什么, 著名的鸭子理论对解释面向协议很有用:"如果一个动物像鸭子一样游泳,像鸭子一样叫,就把它当做鸭子".
代理
在ObjC里面, 协议在代理上面用的范围十分广泛, 在Swift中同样如此, 直接看官方的例子:
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)
}
// 后面的实现很长, 直接截取使用代理的部分:
...
var delegate: DiceGameDelegate? // <--- 声明一个代理的对象
func play() {
delegate?.gameDidStart(self)
...
delegate?.game(self, didStartNewTurnWithDiceRoll: diceRoll)
...
delegate?.gameDidEnd(self)
}
// Optional Chain在这里用的简直不能舒服了...
// 下面是代理的实现类
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")
}
}
用拓展添加协议的一致性
Swift2.0允许我们队协议本身进行拓展, 即便没有权限来访问源代码. 当然, 这要求你必需提供默认实现(不提供的话, 原来的代码不就编译不过了么), 如:
protocol TextRepresentable {
var textualDescription: String { get }
}
extension Dice: TextRepresentable {
var textualDescription: String {
return "A \(sides)-sided dice"
}
}
let d12 = Dice(sides: 12, generator: LinearCongruentialGenerator())
print(d12.textualDescription) // <-- 打印A 12-sided dice
有一种情况比较特殊, 就是实现者本身就实现了某个协议, 但是它并没有声明说实现这个协议, 那么我们可以用extension帮它声明, 提供空实现即可, 如:
struct Hamster {
var name: String
var textualDescription: String {
return "A hamster named \(name)"
}
}
extension Hamster: TextRepresentable {}
这种情况在用别人的代码的时候可能会出现, 如果用别人用OOP写代码, 自己用POP写, 函数传参都是用协议类型, 那么不这么做就会报错.
集合中的协议类型
可塞到某个集合中的对象一般要加一些限定条件, 比如[Int],[String]等, 也可以是更加宽泛的条件, 比如[AnyObject]等. 既然协议可以当做为类型, 那么就可以作为集合的限定条件.如:
let things: [TextRepresentable] = [game, d12, simonTheHamster]
协议继承和组合
和ObjC一样, 协议也可以继承, 不过不同的是, 并不要求每个协议都必须继承一个根协议.
大概是这么样的, 并没有什么区别:
protocol PrettyTextRepresentable: TextRepresentable {
var prettyTextualDescription: String { get }
}
组合就好玩多了, 可以把多个协议组合成一个, 这只是一个语法糖而已, 并不会多造出一个新的协议, 如:
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)
用上typealias一起用可以让可读性更好, 如:
typealias SomeTextProtocol = protocol<SomeProtocol, TextRepresentable>
检查协议一致性
如上面的小节所说, 协议是可以看做类型的, 所以也可以自然会面对笔记十中所要面对的类型转换和检查的问题. 同样, 协议的检查和转换也是用is和as来完成的:
1). is操作符在实例实现了协议的时候返回true, 否则返回false
2). as?操作符返回可选的协议类型值, 如果该实例没有实现协议则返回nil
3). as!操作符强制转换为目标协议类型, 如果失败则会造成runtime error
如果对笔记十的类型章节理解透彻的话, 这边也差不多, 直接看例子吧:
protocol HasArea {
var area: Double { get }
}
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 { // <-- 没有实现HasArea协议
var legs: Int
init(legs: Int) { self.legs = legs }
}
// 构造数组
let objects: [AnyObject] = [
Circle(radius: 2.0),
Country(area: 243_610),
Animal(legs: 4)
]
// 遍历数组
for object in objects {
if let objectWithArea = object as? HasArea {
print("Area is \(objectWithArea.area)")
} else {
print("Something that doesn't have an area") // <--Animal的实例会执行到这里
}
}
可选的协议要求
和ObjC里面一样, 可以要求某些方法是可选的, 这些方法不要求协议的实现者必须实现, 如UITableViewDataSource里面的numberOfSectionsInTableView:方法, 如果不实现默认会返回1.
在Swift里面, 如果一个方法或者属性被标记为optional之后, 其类型也会被转换为optional. 这其实很好例子, 毕竟对于实现者来说, 这个值可能存在也可能不存在, 自然就会是optional类型的了.
需要注意的是, 如果协议里面使用了optional修饰符, 就必须要用@objc来修饰整个协议, 例如:
@objc protocol CounterDataSource {
optional func incrementForCount(count: Int) -> Int
optional var fixedIncrement: Int { get }
}
具体的要去看另外一份文档---- Using Swift with Cocoa and Objective-C (Swift 2.1), 这个有机会也一起聊聊里面有意思的东西, 毕竟在很长的一段时间内, Objc和Swift的代码都会共存.
另外, 如果一个协议用@objc修饰了, 那么它只能被从ObjC中继承的类和其它被@objc修饰的类所实现了.
下面一个例子用了optional chaining来访问optional protocol的属性和方法:
@objc protocol CounterDataSource {
optional func incrementForCount(count: Int) -> Int
optional var fixedIncrement: Int { get }
}
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
}
}
}
// 实现类
class ThreeSource: NSObject, CounterDataSource {
let fixedIncrement = 3
}
// 使用
var counter = Counter()
counter.dataSource = ThreeSource()
for _ in 1...4 {
counter.increment()
print(counter.count)
}
官方文档上还有另外一个例子, 有兴趣可以去看看.
拓展协议
如开篇所说, Swift2.0已经可以拓展协议了, 拓展的协议里的新属性和新方法必须提供默认实现(所以, 属性的set是没法拓展了). 关于默认实现, 如果extension里面的默认实现在原本的实现者里面就已经有了, 那么以实现者本身的为准.
为协议拓展添加约束
有时候, 我们想拓展一个协议, 但是需要加以一定的限制, 例如还需要满足另外一个协议的情况下才允许使用此拓展, 就需要用到约束了. 约束通过where来添加, 这个在下一章Generic里面会讲, 以一个例子来说明:
// Generator.Element就是集合内部的元素, 所以集合起来就是, 当集合内部元素实现了TextRepresentable协议的时候, 可以使用这个拓展
extension CollectionType where Generator.Element: TextRepresentable {
var textualDescription: String {
let itemsAsText = self.map { $0.textualDescription }
return "[" + itemsAsText.joinWithSeparator(", ") + "]"
}
}
// 使用
let murrayTheHamster = Hamster(name: "Murray")
let morganTheHamster = Hamster(name: "Morgan")
let mauriceTheHamster = Hamster(name: "Maurice")
let hamsters = [murrayTheHamster, morganTheHamster, mauriceTheHamster]
print(hamsters.textualDescription) // <--打印 "[A hamster named Murray, A hamster named Morgan, A hamster named Maurice]"
需要提一句的是, 如果需要满足多个条件, 用逗号(,)分隔开, 或者用上面说的protocol组合形式. 暂未发现可以用或(||)条件来满足的, 如果有, 会更新到这里的.
协议的基础就到这里, 具体细节参考官方文档