Swift学习笔记(十一)--拓展和协议

终于到了最让人激动的特性了, 拓展和协议是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组合形式. 暂未发现可以用或(||)条件来满足的, 如果有, 会更新到这里的.

协议的基础就到这里, 具体细节参考官方文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容