Nebula's Secret

(译)高阶函数:Map,Filter,Reduce等

原文: Higher Order Functions: Map,Filter,Reduce and more

闭包介绍

Swift强大的特性之一就是提供了简洁的一等函数/闭包语法,用来替代之前十分复杂的block语法,所以希望我们以后在Swift中不要再使用类似fuckingblocksyntax的语法

闭包是一个自包含的代码块,能够在代码中传递和使用

在本章节中,我们先将重点放在匿名定义的闭包上(即是内联函数且不具名),也称作匿名闭包,我们可以像参数一样传递闭包,也可以把闭包当做返回值返回。闭包是极其强大的语言特性,能让我们编程更迅速,更简便,更少出错

闭包/Block(两种叫法意义一样)广泛应用于Cocoa和Cocoa Touch 中,是iOS frameworks 的核心

先让我们看一些例子,来了解为啥闭包如此有用

现在我们需要两个函数.一个用来求两个数的平方和的平均值,一个用来求2个数的立方和的平均值,一般写法将如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func square(a:Float) -> Float {
return a * a
}
func cube(a:Float) -> Float {
return a * a * a
}
func averageSumOfSquares(a:Float,b:Float) -> Float {
return (square(a) + square(b)) / 2.0
}
func averageSumOfCubes(a:Float,b:Float) -> Float {
return (cube(a) + cube(b)) / 2.0
}

不难观察出,在averageSumOfSquares 和 averageSumOfCubes函数中唯一不同的就是分别调用square/cube方法。如果我们可以定义一个通用函数,这个函数接收两个数字参数和一个使用这两个数字来求平均值的函数参数,用来代替重复调用,这将会更好.这种时候我们可以使用闭包作为参数

1
2
3
4
5
6
func averageOfFunction(a:Float,b:Float,f:(Float -> Float)) -> Float {
return (f(a) + f(b)) / 2
}
averageOfFunction(3, 4, square)
averageOfFunction(3, 4, cube)

上面函数中我们对调用的闭包给了一个明确的名字,通常我们也可以使用闭包表达式来定义一个没有名字的闭包

在swift中有多种定义闭包表达式的方式,在这里,我们会从最详细讲到最简单

1
averageOfFunction(3, 4, {(x: Float) -> Float in return x * x})

(x: Float) -> Float表示了闭包的类型(该闭包接受一个float值作为参数,并且返回一个float值),return x * x是函数具体实现,紧跟在关键字in后面,这种完整写法估计要让人头疼,让我们简化下

首先我们可以省略类型声明,因为能从averageOfFunction函数声明中推断出来(编译器已经知道averageOfFunction函数接收一个接受float和返回float值的闭包)

1
averageOfFunction(3, 4, {x in return x * x})

我们还可以忽略return关键字(注:单行表达式闭包可以通过省略 return 关键字来隐式返回单行表达式的结果)

1
averageOfFunction(3, 4, {x in x * x})

最后,我们还能通过使用默认参数名$0来忽略指定参数名(如果函数接受大于1个的参数,我们将使用$k来定义第(k-1)个参数,如$0,$1,$2)

1
averageOfFunction(3, 4, {$0 * $0})

接下来整篇我们将使用最后这种闭包表达式,实际编码中如果要求清晰明了,建议使用命名闭包。

把闭包作为参数传递而非重复代码,这个编程方式极大地提高了代码的表达能力,同时避免了简单复制和黏贴代码带来的的错误

数组操作

Swift的标准函数库中默认包含支持了三个高阶函数: map, filter 和 reduce,OC的NSAarry缺少这样的支持,但能通过开源项目达到一样的效果

Map

map函数主要用来对每个数组元素通过某个方法进行转换

1
[ x1, x2, ... , xn].map(f) -> [f(x1), f(x2), ... , f(xn)]

现在我们有一个存储一些钱数的int数组,现在想要对数组中每个钱数后拼接一个“€”符号,从而得到一个新的字符串数组

也就是

1
[10,20,45,32] -> ["10€","20€","45€","32€"]

一般不优雅的方式就是创建一个新的空数组,然后遍历和转换数组每个元素,再把结果添加到新数组

1
2
3
4
var stringsArray : [String] = [] //注意,在这里我们要声明数组的存储类型,否则会报类型错误
for money in moneyArray {
stringsArray += "\(money)$"
}

操作并改变数组中的每个元素并创建一个新数组用来存储它们,这是很常见的做法,现在我们可以用map来试试

在swift中 map 是array类声明的一个方法,其函数定义为func map<U>(transform: (T) -> U) -> U[],也就意味着map接收一个命名为transform的函数, 该函数接受一个数组元素T然后返回一个转换后的元素U,map返回值为存储所有转换后对应元素U的数组

在我们例子中T指代int,U指代string,所以我们需要传给map函数一个对应int转变为string的方法

map使用:

1
stringsArray = moneyArray.map({"\($0)€"})

其中{"\($0)€"}就是我们提供的对应int转变为string的闭包

也可以对闭包中的参数具体命名:

1
stringsArray = moneyArray.map({money in "\(money)€"})

上面代码如果你觉得很难了解,那可能是你不了解字符串插值,文档里面对字符串插值有这样一段描写:

字符串插值是在字符串中插入常量,变量,字符,表达式从而生成一个新的字符串方法,插入字符串的每个值都要包含在一对括号中,且带着前缀反斜杠/

Swift String Apple Reference

Filter

filter函数主要用来从数组中选择出符合条件的元素

还是使用上面的例子,现在我们需要写一个函数用来获取一个包含超过30€的钱数的数组

比较naive的写法

1
2
3
4
5
6
7
var filteredArray : [Int] = []
for money in moneyArray {
if (money > 30) {
filteredArray += [money]
}
}

很容易看出,比较核心的部分是money > 30,用filter函数能简明扼要的实现相同的逻辑.

在Swift中filter 是Array类中声明的一个方法, 其函数定义为 func filter(includeElement: (T) -> Bool) -> T[],也就是接收一个返回true或者false 的 includeElement 方法,并对数组每个元素调用该方法, 只有返回true的元素才会存储到新数组

过滤数组,我们只需使用

1
filteredArray = moneyArray.filter({$0 > 30})

{$0 > 30} 是我们提供给filter函数的 闭包

Reduce

reduce方法是用来组合数组中的元素并生成一个新的元素

继续使用上面的例子,这次我们要写一个对数组求和的方法
我们写的代码将得到结果: 107 (10 + 20 + 45 + 32)

比较naive的做法:

1
2
3
4
var sum = 0
for money in moneyArray {
sum = sum + money
}

现在我们看看数组的元素相乘

1
2
3
4
var product = 1
for money in moneyArray {
product = product * money
}

比较这两个函数,唯一不同的是初始值(sum的初始值为0,而product的为1),以及运算符(sum的运算符为+,product的为*)

Reduce可用来快速完整这类操作,通过指定一个初始值和一个用来组合元素的方法

在Swift中reduce 是Array类中声明的一个方法, 其函数定义为 func reduce(initial: U, combine: (U, T) -> U) -> U,也就是接收一个U类型元素的初始值,以及一个把U类型元素和T类型元素组合为一个U类型的闭包, 最后整个数组会Reduce为一个U类型的元素并返回

上面的求和例子中,UT都是int型,初始值为0,combine闭包就是把两个int相加

sum函数用reduce可以这样写

1
sum = moneyArray.reduce(0,{$0 + $1})

在Swift中运算符方法的计算结果作为单行的闭包返回值时,能直接省略参数名

1
sum = moneyArray.reduce(0,+)

Reduce在三个函数中属于比较难理解的函数,要注意的是combine闭包接受的两个参数,第一个类型跟最终结果的类型一致,而第二个参数类型跟数组元素类型一致

最后一点值得注意的是,在数据量比较大的时候,高阶函数实现会比传统实现更快,因为高阶函数能并行运行(即运行在多核上),除非要实现更高性能的map,filter,reduce,否则可以一直使用它们以能获得更快的执行速度

希望你们能明白使用 map, filter, reduce能改善代码的质量,但是也需要在合适的时候使用它们,不要期望它们能帮你解决任何问题,毕竟没有一劳永逸的方法

这里有些问题请你亲自尝试使用闭包解决

  • 1:编写一个函数applyTwice(f:(Float -> Float),x:Float) -> Float,以函数f和x为参数,然后用x参数调用f两次,即f(f(x))

  • 2:编写一个函数applyKTimes(f:(Float ->Float),x:Float,k:Int) -> Float,以函数f和x为参数,然后用x参数调用f函数k次

  • 3:使用applyKTimes编写一个计算出x的k次幂的函数

  • 4:给定一个user实例(user类拥有两个两个变量,age:Int和name:String)组成的数组,用map函数返回一个包含user中所有名字的数组

  • 5:给定一个由字典(字典包含 “name” 和 “age”两个key)组成的数组,用map函数返回一个通过该数组元素字典中的name和age创建的user数组

  • 6:给定一个Number数组,用filter函数选出奇数

  • 7:给定一个字符串数组,用filter函数选出能转换为数字的字符

  • 8:给定一个UIView数组,用filter函数选出UILabel的子类

  • 9:用reduce函数把String数组中元素连接为一个字符串,其中每个元素用换行符分隔

  • 10:用reduce函数从给定的Int数组中选出值最大的数

  • 11:为何不能用reduce实现平均函数{$0 + $1 / Float(array.count)}

  • 12:实现一个类似reduce的函数时,有什么变量可以使操作更简单

  • 13:实现丘奇数(问题比较难,且有多种解法)

敬请期待:-)

示例代码

如果你觉得该文章让你有收获,请尽情分享