Swift 烧脑体操(三) - 高阶函数

2016 年 2 月 23 日

前言

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

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

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

准备运动:基础知识

在上一节里面,我们其实已经涉及到了高阶函数了。在 Wikipedia 中,是这么定义高阶函数(higher-order function)的,如果一个函数:

  • 接受一个或多个函数当作参数
  • 把一个函数当作返回值

那么这个函数就被称作高阶函数。下面是一个简单的排序的例子,在这个例子中,传进去的参数就是一个函数:

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

Trailing Closure Syntax

上面的代码看着不像是函数作为参数存在,这是因为 Swift 的 Trailing Closure 特性。Swift 允许当函数的最后一个参数是闭包的时候,以紧跟 { } 的形式,将最后一个闭包的内容附加在函数后面。

所以,以下两行代码是等价的:

复制代码
// 正常写法,函数是作为 sort 的参数
arr.sort({ $0 < $1 })
// Trailing Closure 写法,更简洁明了
arr.sort { $0 < $1 }

常见用法示例

高阶函数在 Swift 语言中有大量的使用场景,我们先来看一看常见的用法:

遍历

我们可以用 map 方法来对数组元素进行某种规则的转换,例如:

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

求和

我们可以用 reduce 方法,来对数组元素进行某种规则的求和(不一定是加和)。

复制代码
let arr = [1, 2, 4]
// arr = [1, 2, 4]
let brr = arr.reduce(0) {
(prevSum: Int, element: Int) in
return prevSum + element
}
// brr = 7
let crr = arr.reduce("") {
if $0 == "" {
return String($1)
} else {
return $0 + " " + String($1)
}
}
// crr = "1 2 4"

筛选

我们可以利用 filter 方法,来对数组元素进行某种规则的过滤,例如:

复制代码
let arr = [1, 2, 4]
// arr = [1, 2, 4]
let brr = arr.filter {
$0 % 2 == 0
}
// brr = [2, 4]

遍历

即使是以前最简单的遍历,我们也可以用高阶函数的写法,将遍历需要的操作,以函数参数的形式传入 forEach 方法中,例如:

复制代码
let arr = [1, 2, 4]
arr.forEach {
print($0)
}

烧脑体操

下面我们来看看高阶函数一些比较烧脑的细节。

用高阶函数来隐藏私有变量

高阶函数使得代码逻辑可以用函数为主体来进行封装,下面我将详细解释一下这句话。

在面向对象的世界里,逻辑存在的基本单元是对象,每个对象代表着一个最小可复用模块。在对象的内部,由高内聚的成员变量和成员函数构成。这些函数相互调用,并且操作对象的内部成员变量,最终对外产生可预期的行为。

但是利用高阶函数,我们可以同样做到与对象类似的,高内聚的成员变量和成员函数,下面我就举一个具体的例子。

下面的代码中,我们用类的方式,实现了一个 Clock 类, Clock 类实现了一个 getCount 方法,每次调用的时候返回的值+1。为了测试代码,我们定义了两个实例 c1 和 c2,它们都可以正常输出预期的值。

复制代码
class Clock {
var count: Int = 0
func getCount() -> Int {
return ++count;
}
}
let c1 = Clock()
c1.getCount() // 得到 1
c1.getCount() // 得到 2
let c2 = Clock()
c2.getCount() // 得到 1

那么接下来,我们用高阶函数的方式,来做一下同样的事情。我们先看代码:

复制代码
func getClock() -> () -> Int {
var count: Int = 0
let getCount = { () -> Int in
++count;
}
return getCount
}
let c1 = getClock()
c1() // 得到 1
c1() // 得到 2
let c2 = getClock()
c2() // 得到 1

在上面的代码中,我们这里定义了一个 getClock 函数,这个函数可以返回一个 getCount 函数。然后,不太一样的地方是,这个 getCount 函数持有了一个外部的变量 count。于是,这个函数也变得有了状态(或者你也可以说它有了 Side Effect)。每次调用这个函数的时候,返回的值都会变化。

另一方面,因为count变量是 getClock 这个高阶函数的内部变量,所以它并没有像全局变量一样使得封装性被打破。getClock函数仍然可以看作一个高内部的可复用模块,并且对外隐藏了实现细节。

所以,Swift 语言的高阶函数以及闭包可以 capture 外部变量的特性,使得代码逻辑可以以函数作为主体来进行封装,这将使得我们的代码组织更加灵活。

当然,如果滥用,这也会造成代码组织变得更加混乱。

面试题

另一个烧脑的故事是来自于一个朋友的面试题。在面试中,面试官要求他用数组的 reduce 方法实现 map 的功能。

这个题目实在是非常蛋疼,不过用来烧脑倒是不错,大家感兴趣的话可以先想想,再翻下面的参考答案。

复制代码
let arr = [1, 3, 2]
let res = arr.reduce([]) {
(a: [Int], element: Int) -> [Int] in
var t = Array(a)
t.append(element * 2)
return t
}
// res = [2, 6, 4]

不过说回来,虽然这道题目有些奇怪,但是它确实考查了对于高阶函数灵活使用以及对 reduce 方法的理解。大家还可以试试这些题目:

  • 问题一:用 reduce 方法找出数组中的最大值。
  • 问题二:用 reduce 方法一次求出数组中奇数的和、以及偶数乘积。

以下代码是刚刚问题二的参考答案:

复制代码
let arr = [1, 3, 2, 4]
let res: (Int, Int) = arr.reduce((0, 1)) {
(a :(Int, Int), element: Int) -> (Int, Int) in
if element % 2 == 0 {
return (a.0, a.1 * element)
} else {
return (a.0 + element, a.1)
}
}
// res = (4, 8)

高阶函数另一个魔力就是可以链式调用,大家可以尝试这么一道题目:求一个数组中偶数的平方和。

以下是参考答案:

复制代码
let arr = [1, 3, 2, 4]
let res = arr.filter {
$0 % 2 == 1
}.map {
$0 * $0
}.reduce(0) {
$0 + $1
}

总结

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

  • 学习了 Swift 语言中的一些使用高阶函数的示例,包括 map, reduce, filter 等。
  • 学习了利用高阶函数来构造以函数为主体的功能模块。
  • 练习了一些奇怪的面试题。
2016 年 2 月 23 日 00:103878
用户头像

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

关注

评论

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

【Python】__name__ 是什么?

Leetao

Python Python基础

干货 | 如何评估Kubernetes持久化存储方案

焱融科技

Kubernetes 容器 云原生 k8s 容器存储

计算机操作系统基础(十四)---线程同步之条件变量

书旅

php laravel 操作系统 进程 线程’

计算机中短期学习路线

zack

让Go“恐慌”的十种方法

博文视点Broadview

go

啃碎并发(二):Java线程的生命周期

猿灯塔

week05 学习总结 分布式缓存&消息队列&负载

Z冰红茶

女同事问哪吒什么是 Spring 循环依赖?我...

通天哪吒

啃碎并发(三):Java线程上下文切换

猿灯塔

一口气说出 OAuth2.0 的四种授权方式

程序员内点事

Java oauth2.0

Java 线程池中的线程复用是如何实现的?

武培轩

Java 程序员 后端 线程池 源码解析

用进废退,增加能力熟练度与经验值,让你的技能再次精进。

叶小鍵

超详细!一文带你了解 LVS 负载均衡集群!

JackTian

Linux 负载均衡 运维 LVS 服务器集群

架构师训练营第五周作业

一剑

架构师训练营第五周课后总结

Cloud.

一致性 hash 算法

Z冰红茶

一致性Hash算法

一次非常有意思的 SQL 优化经历: 从 30248.271s 到 0.001s

Java小咖秀

MySQL 面试 经验分享 优化逻辑 后端开发

一致性hash的理解与实现

dongge

这份高考卷,只有程序员能得满分...

程序员生活志

程序员 高考

架构师训练营第5周

大丁💸💵💴💶🚀🐟

话题讨论|作为一名程序员,你下班之后都会做些什么?

InfoQ写作平台

写作平台 话题讨论 话题

「深度解析」AI训练之数据缓存

焱融科技

人工智能 AI 存储 焱融科技 数据缓存

Ceph数据恢复初探

焱融科技

焱融科技 文件存储 分布式存储 数据恢复 Ceph

游戏夜读 | 关卡设计的难点

game1night

第五周作业

Linuxer

极客大学架构师训练营

数据分析师成长体系漫谈--数据埋点

analysis-lion

数据分析 数据采集 埋点

SpringBoot 中使用 Filter 的正确姿势

Java课代表

Spring核心原理解析

Chank

Java spring

很多人毕业多年以后,还是改不掉学生思维

小智

职场 思维方式 高考

第 5 周作业:一致性 Hash 算法

姜 某某

第5周结构师训练营——作业

jiangnanage

Swift 烧脑体操(三) - 高阶函数-InfoQ