Swift的面向协议编程

看到了Raywederlich的这篇文章,觉得写得简单易懂, 为了更加深刻的理解也希望把这篇文章推广给更多的人,在此翻译出来:

在WWDC2015中,苹果发布了Swift 2.0,它包含了一些新的特性 以便于我们更好地编写代码.
这些激动人心的特性之一就是 协议扩展. 在Swift的第一个版本中, 我们可以为 , 结构体枚举 添加扩展. 现在在Swift 2.0中,你同样可以为一个 协议 添加扩展.
它乍看上去像是一个不那么重要的特性,但是协议扩展是一个非常强大的特性并且可以改变你编码的方式. 在这个教程中, 我们将会探索创建和使用协议扩展的方式,苹果开放的新技术以及面向协议编程模式.
你也会看到Swift团队是如何用协议扩展来完善Swift标准库的.

开始

创建一个playground, 名字也替你想好了,不用纠结,就叫SwiftProtocols吧.你可以选择任何平台,因为这个教程中的代码是跨平台的.
playground打开之后,添加以下代码:

protocol Bird {
    var name: String { get }
    var canFly: Bool { get }
}

protocol Flyable {
    var airspeedVelocity: Double { get }
}

这几句代码定义了一个简单的 Bird 协议, 它拥有 namecanFly 两个属性; 和一个 Flyable 协议,有一个 airspeedVelocity 属性.

在面向对象的世界里, 你可能会定义 Flyable 作为基类,然后让 Bird 还有其他能飞的东西继承自它, 比如飞机.但是在面向协议的世界中, 一些事物是以协议为起始的.

在下面定义真正类型的时候你将会看到面向协议是如何让整个系统更加灵活的.

定义遵循协议的类型

添加下面的结构体到playground的底部:

struct FlappyBird: Bird, Flyable {
    let name: String
    let flappyAmplitude: Double
    let flappyFrequency: Double
    let canFly = true

    var airspeedVelocity: Double {
        return 3 * flappyFrequency * flappyAmplitude
    }
}

上面的👆代码定义了一个新的结构体 FlappyBird , 它遵循了 BirdFlyable 协议.它的 airspeedVelocity 是一个跟 flappyFrequencyflappyAmplitude 有关的计算型属性. 既然是* flappy (意为飞扬的), 它的 canFly *属性必然为true. :]

下一步, 添加下面的两个结构体到playground的底部:

struct Penguin: Bird {
    let name: String
    let canFly = false
}

struct SwiftBird: Bird, Flyable {
    var name: String { return "Swift\(version)" }
    let version: Double
    let canFly = true  

    // Swift is FAST!
    var airspeedVelocity: Double { return 2000.0 }
}

一个 Penguin (企鹅)是一个* Bird* (鸟类), 但是是不能飞的鸟类.A-ha~ 辛亏我们没有使用继承关系,把* Bird* 写成类继承自Flyable,让所有的鸟类都必须会飞,要不然企鹅得有多蛋疼.一个* SwiftBird (swift在英文中是雨燕的意思,因为雨燕飞的很快,所以swift也有迅速之意)当然是拥有高的 airspeed velocity* 非常快的鸟(傲娇脸).
现在你已经能看到一些代码冗余.每一个遵守* Bird* 协议的类型都必须声明它是不是* canFly* , 尽管你的系统中已经有了一个* Flyable* 的概念.

用默认行为来扩展协议

你可以用协议扩展定义一个协议的默认行为.添加下面的代码到* Bird* 协议下面:

extension Bird where Self: Flyable {
    // Flyable birds can fly!
    var canFly: Bool { return true }
}

这个* Bird* 扩展让所有同样遵循了 Flyable 协议的类型的canFly属性返回true. 也就是说, 遵守了 Flyable 的* Bird* 都不用再明确地声明* canFly* 了.

protocols-extend-480x280.png

Swift1.2在if - let 的绑定使用中引入了where语法.Swift2.0让我们在我们的协议扩展需要一个约束条件时同样能够使用它.
在* FlappyBird* 和 * SwiftBird* 结构体中删除* let canFly = true* . playgroud运行良好, 因为协议扩展已经替你处理了那个需求.

为什么不用基类?

协议扩展和默认实现有些像基类或者其他语言中的抽象类, 但是它在Swift中有一些核心优势:

  • 类型可以遵循多个协议,所以他们可以有很多默认行为. 不像其他语言支持的类的多继承, 协议扩展不会带来额外的状态(这里因为对多继承不是很了解,所以直接翻译了,有人给我推荐了喵神的这篇文章,提到了多继承的菱形缺陷, 不知道这里所谓的额外状态是不是指菱形缺陷,比较懂的朋友还请指点一二)
  • 除了类,结构体和枚举也可以使用协议.而基类和继承只局限于类,结构体和枚举用不了

换句话说,协议扩展让值类型可以拥有默认行为.

上面我们已经说了协议扩展在结构体中的使用,下面来看看枚举, 将下面的代码加到playground底部:

enum UnladenSwallow: Bird, Flyable {
    case African
    case European
    case Unknown
    
    var name: String {
        switch self {
        case .African:
            return "African"
        case .European:
            return "European"
        case .Unknown:
            return "What do you mean? African or European?"
        }
    }
    
    var airspeedVelocity: Double {
        switch self {
        case .African:
            return 10.0
        case .European:
            return 9.9
        case .Unknown:
            fatalError("You are thrown from the bridge of death!")
        }
    }
}

只是换了个类型而已,和结构体没太大区别,不细述.

你不会真的认为这篇教程用到的* airspeedVelocity* 不是用的蒙提派森的梗吧?😄 (这里解释一下,* Monty Phython* 又译为巨蟒剧团、蒙提巨蟒、踎低喷饭,是英国的一组超现实幽默表演团体.而上面的* UnladenSwallow* 枚举是源自于他们一个剧的对话,感兴趣的可以去搜下)

扩展协议

协议扩展最常用的就是扩展外部协议, 不论它是定义在Swift标准库中还是第三方库中
将下面👇的代码加到playground的底部:

extension Collection {
    func skip(skip: Int) -> [Generator.Element] {
        guard skip != 0 else { return [] }
        
        var index = self.startIndex
        var result: [Generator.Element] = []
        var i = 0
        repeat {
            if i % skip == 0 {
                result.append(self[index])
            }
            //原文中是index = index.successor() Swift3中作了以下改变
            index = self.index(after: index)
            //Swift3中不允许用i++了
            i += 1
        } while (index != self.endIndex)
        
        return result
    }
}

这段代码定义了* CollectionType* (Swift3改成了Collection)的一个扩展,里面添加了* skip(_:)* 方法, 这个方法会跳过所有给定条件的元素,然后返回没有被跳过的元素集合.
CollectionType 是一个被Swift中比如数组,字典这样的集合类型所遵循的协议.这意味着新增的这个方法所有的集合类型都可以用.
又来啦,把下面的代码添加到playground底部:

let bunchaBirds: [Bird] =
    [UnladenSwallow.African,
     UnladenSwallow.European,
     UnladenSwallow.Unknown,
     Penguin(name: "King Penguin"),
     SwiftBird(version: 2.0),
     FlappyBird(name: "Felipe", flappyAmplitude: 3.0, flappyFrequency: 20.0)]

bunchaBirds.skip(skip: 3)

这里我们定义了一个鸟类的数组,因为数组遵守了* CollectionType* 的协议,所以也能调用* skip(_:)* 方法.

扩展你自己的协议

我们不光可以像上面一样给标准库中的协议扩展方法,还可以添加默认行为.
修改* Bird* 协议生命让它遵循* BooleanType* 协议:

protocol Bird: BooleanType {

遵守* BooleanType* 协议意味着你的类型得有一个* boolValue* . 难道我们要把这个属性添加到每一个* Bird* 类型吗?😱
当然不,将下面的代码添加到* Bird* 定义下面:

extension BooleanType where Self: Bird {
    var boolValue: Bool {
        return self.canFly
    }
}

这个扩展让* canFly* 属性代表了每个* Bird* 类型的布尔值.
看这个是否成立,我们试试下面的代码,同样加到playground底部:

if UnladenSwallow.African {
    print("I can fly!")
} else  {
    print("Guess I’ll just sit here :[")
}

非布尔值是不能直接在if后面做true or false的判断的, 但是这里却可以了,就是因为* Bird* 遵循了* BooleanType* , 而* UnladenSwallow.African* 的* canFly* 值是true, 所以它的* boolValue* 也是true.

对于Swift标准库的影响

上面我们已经看到协议扩展怎样帮助我们自定义和扩展我们的代码功能.更加让你感到惊讶的会是Swift项目组如何运用协议扩展来完善Swift标准库.
Swfit引入了map, reduce, 和 filter 方法来促进函数式编程.这些方法用在集合类型中, 比如数组:

//计算数组中所有元素字符数之和
let result = ["frog","pants"].map { $0.lengthOfBytes(using: .utf8) }.reduce(0) { $0 + $1 }
print(result)

调用数组的* map* 函数返回另外一个数组,这个数组里面盛放的是原数组的每个元素字符数,即[4,5], 这个数组又调用了 reduce 函数来计算二者之和,结果返回9.

Cmd-Click进入 map 函数源码可以看到它的定义.
Swfit 1.2中如下:

// Swift 1.2
extension Array : _ArrayType {
      /// Return an `Array` containing the results of calling 
      /// `transform(x)` on each element `x` of `self` 
      func map<U>(transform: (T) -> U) -> [U]
}

这里 map 函数是定义在 Array 的extension中的, 可是这些功能性的函数不止能被 Array 使用, 它们可以被任何集合类型所调用, 那么Swift 1.2中是怎么实现的呢?
如果你用一个 Range 类型调用 map 方法, 然后Cmd-Click map函数进入源码,能看到下面的代码:

// Swift 1.2
extension Range { 
      /// Return an array containing the results of calling 
      /// `transform(x)` on each element `x` of `self`. 
      func map<U>(transform: (T) -> U) -> [U]
}

结果是Swift 1.2中,所有的集合类型都要定义一遍 map 函数, 这是因为虽然 ArrayRange 都是集合类型,但是结构体是不能被继承的.
这不是一个小差别,它会限制你使用Swift的类型.
下面的泛型函数适用于那些遵循了 Flyable 的集合类型,并返回一个只有一个元素的数组, 这个元素就是集合类型中所有元素的最大 airspeedVelocity :

func topSpeed<T: CollectionType where T.GeneratorType: Flyable>(collection: T) -> Double { 
    collection.map { $0.airspeedVelocity }.reduce { max($0, $1) }
}

Swift 1.2中这个函数会报错, mapreduce 等函数只能被标准库中已经定义的集合类型使用, 像这里我们自己定义的遵守 Flyable 的集合类型是不能使用这些函数的.

Swift 2.0中, map 函数是这样定义的:

// Swift 2.0
extension CollectionType { 
    /// Return an `Array` containing the results of mapping `transform` 
    /// over `self`. 
    /// 
    /// - Complexity: O(N). 
    func map<T>(@noescape transform: (Self.Generator.Element) -> T) -> [T]
}

这样所有的集合类型都能使用 map 函数了.

将上面的泛型函数加到playground的底部:

func topSpeed<T: CollectionType where T.GeneratorType: Flyable>(collection: T) -> Double { 
    collection.map { $0.airspeedVelocity }.reduce { max($0, $1) }
}

运行良好😊 . 现在我们可以看看到底这些鸟类中谁是最快的! :]

let flyingBirds: [Flyable] = 
  [UnladenSwallow.African, 
  UnladenSwallow.European, 
  SwiftBird(version: 2.0)] 

topSpeed(flyingBirds) // 2000.0

还用说😉 .

后话: 终于翻译完了,累成狗🐶 . 看在我这么拼的份上,转载请注明出处:]

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

推荐阅读更多精彩内容