Swift 2之协议式编程介绍「译」


Swift Bird brings speedy new features to Swift 2!


备注:此教程需要安装Xcode 7及Swift 2,此时两者均可能处于beta版本。您可以通过苹果开发者门户网站下载最新的beta版本。『译者注:原文发布于2015.6.25,当时Xcode 7及Swift 2尚处于beta版本』。

在WWDC2015,苹果发布了包含众多新语言特性的Swift 2,帮助开发者改善他们的编码方式。

其中,最让人兴奋的要属协议扩展(protocol extensions)语法。在Swift 1中,开发者可以扩展原有的class, struct和enum类型。现在,开发者可以对protocol进行扩展了。

刚看到protocol extensions时,大家可能认为这只是一个相对次要的功能,但实际上protocol extensions非常强大,强大到甚至可以改变开发者的编码方式!在这个教程中,你将会跟随我们一起探索如何使用protocol extensions语法,以及如何进行「协议式编程」

开始

在Xcode中,选择File\New\Playground...打开一个新的playground,并将playground命名为SwiftProtocols。由于本教程可以用在任意平台上,platform可以随意选择。点击Next,保存Playground。
创建成功后,输入下述代码:

protocol Bird {
    var name: String { get }
    var canFly: Bool { get }
}
protocol Flyable {
    var airspeedVelocity: Double { get }
}

这里我们定义了一个简单的protocol Bird,包含属性namecanFly,同时定义了一个Flyable协议,包含属性airSpeedVelocity

在没有protocol的时候,大家一般会将Flyable定义为基类,然后通过继承的方式定义Bird及其他可以「飞」的类,例如飞机。在这里我们需要明确一点,所有的事情都由protocol开始!

当你开始定义类型时,你将会逐步发现「协议式编程」是如何使整个系统更加灵活健壮的。

定义遵循protocol的类型

在playground中,添加下述struct

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两个protocol。其中,属性airspeedVelocity是通过闭包的方式计算返回的。同时,由于可以飞行,所以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
    
    var airspeedVelocity: Double { return 2000.0 }
}

在这里,企鹅Penguin同样属于鸟类,但是不能飞。啊哈~这就体现出不使用类继承的好处了,若使用了类继承的方式,则所有子类中的canFly属性都只能统一了!雨燕SwiftBird是很快的,所以airspeedVelocity属性返回了2000.0,一个非常快的飞行速度。

到此,相信你已经发现一些多余的代码了。每种类型的Bird都要重新定义canFlytrue还是false,尽管Flyable已经暗示了canFly = true

扩展协议,使其包含默认的实现

通过protocol extensions语法,开发者可以给一个protocol定义默认的执行方法。在Bird协议下,添加如下代码:

extension Bird where Self: Flyable {
    var canFly: Bool { return true }
}

在此,我们给协议Bird指定了一个默认的执行方法:「如果该类型包含了Flyable协议,则canFly返回true」。这样,所有Flyable的bird都不需要另外再明确声明canFly = true了。

Swift 1.2中介绍了where语法在if-let中的使用,而Swift 2则给我们带来了有条件地扩展protocol的能力。

接下来,在FlappyBirdSwiftBird结构体中删除let canFly = true语句,你会发现playground运行一切正常,因为,protocol extension已经帮你处理好canFly属性了。

为什么不使用基类

协议扩展(protocol extensions)及其默认执行方法看起来可能跟使用基类和抽象类(abstract type)相似,但Swift有几点优势:

  • 由于类型可以遵循多个protocol,因此可以被多个protocols的默认行为修饰。与多重继承的方式不同,协议扩展不会引入额外的属性及状态。
  • protocol可以被class、struct及enum遵循,而基类和多重继承均只能限制在类之间使用。

换句话说,协议扩展提供了让value类型也能用上默认执行方法,而不仅是类专有了。

按照上述步骤,相信开发者已经知道如何在struct中应用protocol了;接下来,我们尝试在enum类型中应用protocol。请在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!")
        }
    }
}

跟其他value类型一样,在enum中,开发者只需给协议中的属性定义正确的实现。由于UnladenSwallow同时遵循BirdFlyable,故canFlay属性不需要定义其实现,默认为true
至此,你还认为本教程中的airspeedVelocity不会包含一个蟒蛇的引用吗?『译者注:水平有限,此处翻译生硬,原文为「Did you really think this tutorial involving airspeedVelocity wouldn't include a Monty Python reference?:]」』

协议扩展的应用

对于开发者来说,协议扩展最常用于对现有协议的扩展,包括Swift原生库及第三方开发者编写的框架。
在playground中,添加如下代码:

extension CollectionType {
    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()
            i += 1
        }while (index != self.endIndex)
        
        return result
    }
}

在此,我们对CollectionType协议进行扩展,添加一个skip(_:)方法,可以把集合中下标能与x整除的元素去除,并返回处理后的集合。

由于CollectionType是一个被arraysdictionaries遵循的协议,所以对CollectionType进行扩展后,arraydictionary都可以使用skip(_:)方法了!在playground中,增加如下代码:

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

bunchaBirds.skip(3)

在此,我们定义了包含了多种鸟类的数组bunchaBirds。由于CollectionType协议扩展了skip(_:)方法,因此我们可以调用skip(_:)方法来对数组进行处理。

扩展你的协议

除了可以扩展Swift原生协议,给Swift原生协议定义默认实现也同样让人激动。

在playground中,把协议Bird修改为:

protocol Bird: BolleanType {

由于遵循了BooleanType协议,因此,在Bird协议中需要添加一个boolValue来表达bool值。这是不是意味着开发者必须在每个遵循Bird协议的类型下都给boolValue设计一个实现呢?

当然,你可以使用协议扩展的方式给boolValue设计一个默认实现。在playground的Bird下方增加如下代码:

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

这样扩展了协议之后,canFly属性就可以充当遵循Bird协议类型的boolValue了。

下面我们通过下述代码进行测试:

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

在此,你会看到I can fly!的打印。但尤其需要注意的是,在代码中,你不过在if语句后使用了类型,并未使用任何额外的判断语句!

协议扩展对Swift原生库的影响

到目前为止,相信你已经体会到协议扩展对开发者代码带来的扩展性。同时,协议扩展也給Swift原生库代码的编写带来了极大的扩展性。

通过遵循map, reduce, filter协议,Swift可以提升原生库的函数式编程模板。这些方法在CollectionType类型中得到不少体现,例如Array类型:

["frog", "pants"].map{$0.length}.reduce(0) { $0 + $1 }

上述代码中,让array调用map方法,得到一个新的array,然后让新array调用reduce方法,得到新array中的元素累加和9.

在此,mapreduce方法都成为了Array的原生方法。如果你按住Cmd并点击map,就可以看到map方法是如何定义的。

在Swift 1.2中,你将看到:

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

这里,map方法被定义为Array类型的扩展。但是,Swift的高阶函数并非只提供Array使用,应该所有的CollectionType都能够使用,那么在Swift 1.2中是如何实现的呢?

如果让Range调用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中,因为struct不能成为子类,也不能包含公用实现,所以必须给所有CollectionType都重新定义map的实现。

这种方式不仅限制了Swift标准库,同时也限制了开发者对Swift中各种类型的使用方式。

下列函数形参为「遵循Flyable协议的CollectionType」,返回值为数值最高的airspeedVelocity

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

由于Swift 1.2没有协议扩展功能,上述代码实际上会引入编译错误。mapreduce只存在于一个具体的定义好的类型中,而非存在于抽象的CollectionType中。

但是,在Swift 2.0 协议扩展的功能下,map方法的定义变成了如下:

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

虽然不能看到看到方法map的源码 —— 至少等到Swift 2正式发布将会是开源的!现在,CollectionType有了一个默认的map实现,所有遵循CollectionType协议的类型都将支持map方法。

接下来,在playground中添加如下代码:

func topSpeed<T: CollectionType where T.Generator.Element == Flyable>(c : T) -> Double {
    return c.map({$0.airspeedVelocity}).reduce(0) { max($0, $1) }
}

上述函数中,mapreduce方法均能被遵循CollectionType协议的类型调用。现在,通过下述代码,即可算出「哪种鸟类飞得最快」了:

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

topSpeed(flyingBirds)

接下来应该做什么

首先,你可以下载本篇教程中完整的playground代码

通过本教程,相信你已经体会到「协议式编程」的威力。你可以自己定义简单的协议然后在使用时根据需要去扩展它,同时,你也可以给先有的协议编写默认的实现,类似于基类但不同于基类的,协议还能够支持structs和enums。

再者,协议扩展不仅可以扩展你自己写的协议,同时也可以扩展Swift标准库里面的协议,Cocoa及Cocoa Touch的协议。

如果想知道Swift 2中还有什么新功能,你可以通过我们的文章「what’s new in Swift 2.0」或者苹果官方博客「Apple’s Swift blog」中查看。

同时,你可以通过观看WWDC的「Protocol Oriented Programming」视频,深入了解「协议式编程」背后的理论。

如果有任何问题?欢迎留言讨论!

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

推荐阅读更多精彩内容