写点什么

Swift 烧脑体操(二) - 函数的参数

  • 2016-02-15
  • 本文字数:4795 字

    阅读完需:约 16 分钟

前言

Swift 其实比 Objective-C 复杂很多,相对于出生于上世纪 80 年代的 Objective-C 来说,Swift 融入了大量新特性。这也使得我们学习掌握这门语言变得相对来说更加困难。不过一切都是值得的,Swift 相比 Objective-C,写出来的程序更安全、更简洁,最终能够提高我们的工作效率和质量。

Swift 相关的学习资料已经很多,我想从另外一个角度来介绍它的一些特性,我把这个角度叫做「烧脑体操」。什么意思呢?就是我们专门挑一些比较费脑子的语言细节来学习。通过「烧脑」地思考,来达到对 Swift 语言的更加深入的理解。

这是本体操的第二节,练习前请做好准备运动,保持头脑清醒。

准备运动:基础知识

面向对象语言的世界观

对于很多面向对象的编程语言来说,在思考问题时,总是把「对象」作为考虑问题的基本出发点。

面向对象的程序设计通过以下三大规则,构建出程序设计的基础,它们是:

  1. 封装(Encapsulation),将一个相对独立的逻辑涉及的变量和函数放到一个类中,然后对外暴露少量接口,使其高内聚,低耦合。
  2. 继承(Inheritance),子类可以继承父类的变量和函数,并且可以修改或扩展父类的行为。
  3. 多态(Polymorphism),父类的指针可以指向子类的实例,在运行时程序语言支持找到子类对应的函数实现。

在以上三大准则的基础上,再引入一些设计原则,比如:

  1. 单一职责原则(Single Responsibility),每个类只应该做一件事情。
  2. 不要重复原则(Don’t Repeat Yourself),相同(或相似)的代码不应该重复两次。
  3. 好的组合优于继承(Better Composition over Inheritance),尽量使用组合而不是继承来设计。

于是,程序世界就基于这些规则和原则,产生出了设计模式,进而能更加精准地指导我们的编程行为。这就像我们学习几何,先学习几条公理,然后以后的大量定理都通过公理证明而来。

举个例子,单例模式(Singleton Pattern)其实就是封装和单一职责原则的产物。代理模式(Delegate Pattern) 也是单一职责和封装中的面向接口设计的思想的产物。

但是,在面向对象语言的世界观里面,函数都是作为一个附属物存在的。函数通常附属于一个具体类的某个方法中。或许有一个函数它根本都不需要任何对象作为容器,为了这个世界的统一,我们还是会构造一个类,把这个函数放进去。比如,在小猿搜题中,我们就有一个叫 ImageUtils 的类,里面放了操作图像的各种各样的静态方法,有一些图象操作函数其实也不太通用,但是总得找一个类放不是。

在一些面向对象语言的世界中,如果把对象称作 OOP 的一等公民的话,那么函数就是二等公民。

函数式编程

在 Swift 的世界中,函数并不是二等公民。是的,Swift 引入了大量函数式编程的特性,使得我们能够把函数当作一等公民来对待。

一等公民有什么权利呢?那就是函数可以像对象一样,被赋值、被当作参数传递、参与计算或者当作结果被返回。

我们先来看一段函数被赋值的例子,在下例中,我们将一个函数赋值给一个名为 myFunc 的变量,然后调用它。

复制代码
let myFunc = {
() -> String in
return "Tang Qiao"
}
let value = myFunc()
// value 的值为 "Tang Qiao"

我们再来看一个函数被当作运算结果返回的例子。在这个例子中,我们希望构造一个「加法器」工厂,这个工厂能够接受一个参数 addValue,返回一个加法器函数,这个加法器函数能够将传递的参数加 addValue 之后返回。以下是实现的代码:

复制代码
func addFactory(addValue: Int) -> (Int -> Int) {
func adder(value: Int) -> Int {
return addValue + value
}
return adder
}

有了上面这个函数,我们就可以构造一个 +2 的函数,然后使用它,如下所示:

复制代码
let add2 = addFactory(2) // 构造一个 +2 的函数
let result = add2(3) // 运算,传入 3,得到 5

函数的参数

但是在本次「烧脑体操」中,全面介绍函数式编程明显不太现实,所以我们仅从函数的参数来深入学习一下,看看在 Swift 语言中,函数的参数能够有多复杂。

参数的省略

我们先来简单看看函数参数的省略吧,因为有类型推导,函数的参数在 Swift 中常常可以被省略掉,特别以匿名函数(闭包)的形式存在的时候。

我们来看一个数组排序的例子:

复制代码
let array = [1, 3, 2, 4]
let res = array.sort {
(a: Int, b: Int) -> Bool in
return a < b
}

如果一个函数返回类型可以通过推导出来,则返回类型可以省略。所以以上代码中的 -> Bool 可以删掉,变成:

复制代码
let array = [1, 3, 2, 4]
let res = array.sort {
(a: Int, b: Int) in
return a < b
}

如果一个函数的参数类型可以推导出来,则参数的类型可以省略。所以以上代码中的 : Int 可以删掉,变成:

复制代码
let array = [1, 3, 2, 4]
let res = array.sort {
(a, b) in
return a < b
}

如果函数参数的个数可以推导出来,也可以不写参数。那怎么使用这些参数呢?可以用 $0, $1 这样的方式来引用参数。所以以上代码中的 (a, b) 可以删掉,因为这样的话,参数和返回值都省略了,所以in也可以省略了,变成:

复制代码
let array = [1, 3, 2, 4]
let res = array.sort {
return $0 < $1
}

Swift 还有一个规则,如果函数的 body 只有一行,则可以把 return 关键字省略了,所以以上代码可以进一步简化成:

复制代码
let array = [1, 3, 2, 4]
let res = array.sort {
$0 < $1
}

最后一个简化规则更加暴力,因为 < 符号也是一个函数,它接受的参数个数,类型和返回值与 sort 函数需要的一样,所以可以直接简化成:

复制代码
let array = [1, 3, 2, 4]
let res = array.sort( < )

拿这个的方法,同样可以把我们刚刚写的 addFactory 做简化,最后简化成如下的形式:

复制代码
// 简化前
func addFactory(addValue: Int) -> (Int -> Int) {
func adder(value: Int) -> Int {
return addValue + value
}
return adder
}
// 简化后
func addFactory(addValue: Int) -> (Int -> Int) {
return { addValue + $0 }
}

函数参数中的其它关键字

有些时候,我们的函数接受的参数就是另外一个函数,例如 sort,map,所以我们在看代码的时候,需要具备熟悉这种写法的能力。

我们来看看数组的 map 函数的定义吧:

复制代码
public func map<T>(@noescape transform: (Self.Generator.Element) throws -> T) rethrows -> [T]

这个函数定义中出现了几个我们刚刚没提到的关键词,我们先学习一下。

@noescape

@noescape,这是一个从 Swift 1.2 引入的关键字,它是专门用于修饰函数闭包这种参数类型的,当出现这个参数时,它表示该闭包不会跳出这个函数调用的生命期:即函数调用完之后,这个闭包的生命期也结束了。以下是苹果的文档原文:

A new @noescape attribute may be used on closure parameters to functions. This indicates that the parameter is only ever called (or passed as an @noescape parameter in a call), which means that it cannot outlive the lifetime of the call. This enables some minor performance optimizations, but more importantly disables the self. requirement in closure arguments.

什么情况下一个闭包参数会跳出函数的生命期呢?很简单,我们在函数实现内,将一个闭包用 dispatch_async 嵌套,这样这个闭包就会在另外一个线程中存在,从而跳出了当前函数的生命期。这样做主要是可以帮助编译器做性能的优化。

如果你对此感兴趣,这里有一些更详细的介绍供你学习:

throwsrethrows

throws 关键字表示:这个函数(闭包)可能抛出异常。而 rethrows 关键字表示:这个函数如果抛出异常,仅可能是因为传递给它的闭包的调用导致了异常。

throws 关键字的存在大家都应该能理解,因为总有一些异常可能在设计的时候希望暴露给上层,throws 关键字的存在使得这种设计成为可能。

那么为什么会有 rethrows 关键字呢?在我看来,这是为了简化很多代码的书写。因为一旦一个函数会抛出异常,按 Swift 类型安全的写法,我们就需要使用 try 语法。但是如果很多地方都需要写 try 的话,会造成代码非常啰嗦。 rethrows 关键字使得一些情况下,如果你传进去的闭包不会抛出异常,那么你的调用代码就不需要写 try。

如果你对此感兴趣,这里有一些更详细的介绍供你学习:

函数作为函数的参数

刚刚说到,函数作为一等公民,意味着函数可以像对象一样,被当作参数传递或者被当作值返回。对此,我们专门有一个名称来称呼它,叫做高阶函数(higher-order function)

在刚刚那个数组的 map 函数中,我们就看到了它接着另外一个函数作为参数,这个函数接受数组元素类型作为参数,返回一个新类型。

复制代码
public func map<T>(@noescape transform: (Self.Generator.Element) throws -> T) rethrows -> [T]

有了 map 函数,我们就可以轻松做数组元素的变换了。如下所示:

复制代码
let arr = [1, 2, 4]
// arr = [1, 2, 4]
let brr = arr.map {
"No." + String($0)
}
// brr = ["No.1", "No.2", "No.4"]

烧脑的参数

好了,现在进入参数烧脑游戏的正式环节。

我们需要构造一个工厂函数,这个函数接受两个函数作为参数,返回一个新的函数。新函数是两个函数参数的叠加作用效果。

举一个具体的例子,假如我们有一个 +2 的函数,有一个 +3 的函数,那用这个工厂函数,我们可以得到一个 +5 的函数。

又比如我们有一个 *2 的函数,有一个 *5 的函数,用这个工厂函数,我们就可以得到一个 *10 的函数。

那这个函数如何写呢?我们先看答案吧:

复制代码
func funcBuild(f: Int -> Int, _ g: Int -> Int)
-> Int -> Int {
return {
f(g($0))
}
}
let f1 = funcBuild({$0 + 2}, {$0 + 3})
f1(0) // 得到 5
let f2 = funcBuild({$0 * 2}, {$0 * 5})
f2(1) // 得到 10

这个函数充分反映了函数作为一等公民的地位。但是,我们同时也看出来,函数作为参数存在时,对于程序的可读性带来了挑战。好在我们有 typealias,通过 typealias,我们可以将函数的类型写得更加易读,比如上面的代码,就可以修改成如下形式:

复制代码
typealias IntFunction = Int -> Int
func funcBuild(f: IntFunction, _ g: IntFunction)
-> IntFunction {
return {
f(g($0))
}
}

现在看看代码,是不是清晰了很多?

参数中的范型

当函数中的参数再引入范型之后,函数的功能更加强大,但是可读性进一步下降。比如刚刚的例子,限制函数只能是 Int -> Int 其实是没有必要的,我们将两个函数拼成一个函数,只需要保证一个函数的输出类型,与另一个函数的输入类型匹配即可。所以,刚刚的例子,可以进一步用范型改造:

复制代码
func funcBuild<T, U, V>(f: T -> U, _ g: V -> T)
-> V -> U {
return {
f(g($0))
}
}
let f3 = funcBuild({ "No." + String($0) }, {$0 * 2})
f3(23) // 结果是 "No.46"

在上面这个例子中,我们保证函数 g 的输出类型是 T,函数 f 的输入类型是 T。这样,在例子中,我们将一个 *2 的函数与一个数字转字符串的函数拼接起来,构造出一个先乘 2,再转字符串的函数。

相应的例子还有很多,比如 WWDC 中就介绍过一个给函数增加缓存机制的代码,在该代码中,任意一个不带缓存功能的函数,经过改造,都可以变成一个带缓存功能的函数。代码如下,大家可以自行学习一下:

复制代码
func memoize<T: Hashable, U>( body: (T)->U ) -> (T->U) {
var memo = Dictionary<T, U>()
return { x in
if let q = memo[x] { return q }
let r = body(x)
memo[x] = r
return r
}
}

总结

总结一下本次烧脑锻炼到的脑细胞:

  • Swift 是一个结合面向对象编程和函数式编程特性的语言。
  • 函数在 Swift 中是一等公民,可以被赋值、被当作参数传递、参与计算、当作结果被返回或被动态创建。
  • 因为有类型推导,函数的参数有各种省略规则。
  • 函数作为参数时,有 @noescape,throw 和 rethrow 关键字需要了解。
  • 函数作为参数时,不易阅读。合理使用 typealias 可以使源码结构更清晰。
2016-02-15 19:084460
用户头像

发布了 65 篇内容, 共 58.0 次阅读, 收获喜欢 23 次。

关注

评论

发布
暂无评论
发现更多内容

Github瞬间爆火!被各大厂要求直接下架的面试题库也太全了

Java 编程 程序员 架构 面试

前端工程化-webpack

chun1123

大前端 webpack

Dubbo学习笔记

风翱

dubbo 4月日更

什么是低代码开发?低代码11大核心功能介绍!

优秀

低代码

深入浅出解读SpringMVC 进阶版

AI乔治

Java spring 架构 微服务 springmvc

MySQL高可用架构:mysql+keepalived实现

AI乔治

Java MySQL 架构 高可用架构 keepalive

为打造更好移动生态:日活超1.2亿的百度知道,摊牌了

ToB行业头条

百度 百度知道

一枚比特币变换一台车,特斯拉与比特币还要绑定多久?

CECBC

特斯拉

架构师必经之路!Github榜首Java代码优化:77案例+28技巧开源分享

Java架构师迁哥

聪明人的训练(二十七)

Changing Lin

4月日更

电脑护眼不权威指南,年纪轻轻眼睛就不行了?

彭宏豪95

效率 windows 4月日更 护眼

如何在 Spring/Spring Boot 中优雅地做参数校验?

JavaGuide

Java spring springboot

周转箱在仓配业务中的应用

Geek_vidmje

面试别慌!阿里专家带你从【入门+基础+进阶+项目】攻破SpringBoot

Java架构师迁哥

重装变态的微信

箭上有毒

生活记录 4月日更

MySQL主从不一致情形与解决方法

AI乔治

Java MySQL 架构 主从同步

京东首席架构师深邃洞察:服务化+云原生+微服务

Java架构师迁哥

全面复盘B站面试时坑我最深的Java并发:JDK源码剖析

Java架构师迁哥

别让心里的墙,挡住了你未来的路

小天同学

思考 认知提升 认知 4月日更

网络协议学习笔记 Day6

穿过生命散发芬芳

网络协议 4月日更

ElasticSearch

云淡风轻

elasticsearch

答对这八个Redis的问题,说明你精通了

AI乔治

Java redis 架构 分布式 高并发

工行首次全面展示数十项数字人民币研发试点成果

CECBC

数字人民币

强化区块链技术在数字政府、智慧城市、智能制造等领域应用

CECBC

人工智能

开源新老兵携手打开社区大门,带大学生走近开源

DT极客

小技巧 | 帮助运营小姐姐批量修改文件名的几种方式

梁龙先森

Node 脚本 PowerShell

收藏!阿里P9耗时28天,总结历年亿级活动高并发系统设计手册

Java架构师迁哥

为什么区块链技术仍然被人们认为当成一个投机的工具?

CECBC

你公司有没有企业文化?

石云升

团队建设 28天写作 职场经验 管理经验 4月日更

爆肝1W+字,通俗易懂的讲解下Java注解

AI乔治

Java 架构 面试 注解

对于医疗健康行业,区块链最大的价值在哪里?

CECBC

医疗

Swift 烧脑体操(二) - 函数的参数_移动_唐巧_InfoQ精选文章