swift小知识点之高阶函数之map, filter, reduce
初探高阶函数
在 Swift 中,高阶函数一共有下面几个:
-
map:对给定数组每个元素,执行闭包中的映射,将映射结果放置在数组中返回。
-
flatMap:对给定数组的每个元素,执行闭包中的映射,对映射结果进行
合并操作,
然后将合并操作后的结果放置在数组中返回。 -
compactMap:对给定数组的每个元素,执行闭包中的映射,将
非空的
映射结果放置在数组中返回或者将非空的
映射结果-键值对放置在字典中返回。 -
filter:对给定数组的每个元素,执行闭包中的操作,将
符合条件的元素
放在数组中返回。 -
reduce:对给定数组的每个元素,执行闭包中的操作对元素进行
合并
,并将合并结果返回。
Map
map 函数的作用就是对集合进行一个循环,循环内部再对每个元素做同一个操作。它返回一个包含映射后元素的数组。
简单说就是数组中每个元素通过某个方法进行转换,最后返回一个新的数组。
-
Map on Array:
假设我们有一个整数型数组,我们如何让每个数都乘10呢?我们通常使用for-in
来遍历每个元素,然后执行相关操作。
let arrayOfInt = [2,3,4,5,4,7,2] var newArr:[Int] = [] for value in arrayOfInt { newArr.append(value * 10) } print(newArr) /*打印: [20, 30, 40, 50, 40, 70, 20] */
上面的代码看着有点冗余。它包含创建一个新数组的样板代码,我们可以通过使用 map 来避免。我们通过 Swift 的自动补全功能可以看到 map 函数接受有一个 Int 类型的参数并返回一个泛型的闭包。
let arrayOfInt = [2,3,4,5,4,7,2] let newArr = arrayOfInt.map { $0 * 10 } print(newArr) /*打印: [20, 30, 40, 50, 40, 70, 20] */
对于一个整型数组,这是 map 的极简版本。我们可以在闭包中使用
$
操作符来代指遍历的每个元素。
下面的代码作用都是一样的,它们表示了一个从繁到简的过程。通过下面的代码,你应该对闭包有了一个清晰的认识。arrayOfInt.map ({ (someInt:Int) -> Int in return someInt * 10}) //1.闭包语法 arrayOfInt.map ({ (someInt:Int) in return someInt * 10}) //2.省略返回值 arrayOfInt.map ({ someInt in return someInt * 10}) //3.省略指定参数类型 arrayOfInt.map ({ someInt in someInt * 10}) //4.省略return arrayOfInt.map { $0 * 10} //5.尾随闭包语法 /*打印: [20, 30, 40, 50, 40, 70, 20] */
map 的工作原理:map 函数接受一个闭包作为参数,在迭代集合时调用该闭包。这个闭包映射集合中的元素,并将结果返回。map 函数再将结果放在数组中返回。
-
Map on Dictionary:
假设我们有一个书名当做 key ,书的价格当做 value 的字典。如果你试图 map 这个字典,Swift 的自动补全将是这样:
let bookAmount = ["harrypotter":100.0, "junglebook":100.0] let returnFormatMap = bookAmount.map { (key: String, value: Double) in return key.capitalized } print(returnFormatMap) /*打印: ["Junglebook", "Harrypotter"] */
我们通过上面的代码,对一个字典进行遍历,每次遍历在闭包中都有一个 String 类型的 key ,和一个 Double 类型的 value 。返回值为一个大写首字母的字符串数组,数组的值还可以是价格或者元组,这取决于你的需求。
注意:map 函数的返回值类型总是一个泛型数组。你可以返回包含任意类型的数组。
-
Map on set:
let lengthInMeters: Set = [4.0, 6.2, 8.9] let lengthInFeet = lengthInMeters.map { $0 * 3.2808 } print(lengthInFeet) /*打印: [29.199120000000004, 13.1232, 20.340960000000003] */
在上面的代码中,我们有一个值类型为 Double 的 set,我们的闭包返回值也是 Double 类型。
lengthInMeters
是一个 set,而lengthInFeet
是一个数组。
-
如果你想在 map 的时候获取 index 应该怎么做?
答案很简单,你必须在 map 之前调用
enumerate
。下面是示例代码:
let numbers = [1,2,4,5] let indexAndNum = numbers.enumerated().map { (index,element) in return "\(index):\(element)" } print(indexAndNum) /*打印: ["0:1", "1:2", "2:4", "3:5"]*/
Filter
Filter,顾名思义,就是过滤、筛选的意思,对于一个集合对象而言,filter函数的作用是遍历该集合,然后将该集合中符合某些特定条件的元素组成新的数组,并返回该新数组,filter函数就是选择集合中符合条件的元素,过滤掉不符合条件的元素。
filter函数只有一个闭包作为参数,该闭包指定了筛选条件。该闭包使用集合对象中的一个元素作为参数,并且必须返回一个Bool值以表明该元素是否应该被包含新的数组中并返回。
-
Filter on array
假设我们要筛选一个整型数组中包含的偶数,你可能会写下面的代码:
let arrayOfIntegers = [1,2,3,4,5,6,7,8,9] var newArray = [Int]() for integer in arrayOfIntegers { if integer % 2 == 0 { newArray.append(integer) } } print(newArray) /*打印:["0:1", "1:2", "2:4", "3:5"]*/
就像 map ,这是一个简单的函数去筛选集合中的元素。
如果我们对整形数组使用 filter ,Swift 的自动补全展示如下:如你所见, filter 函数调用了一个接受一个 Int 类型参数、返回值为 Bool 类型、名字为
isIncluded
的闭包。isIncluded
在每次遍历都会返回一个布尔值,然后基于布尔值将创建一个新的包含筛选结果的数组。我们可以通过 filter 将上面的代码修改为:
let arrayOfIntegers = [1,2,3,4,5,6,7,8,9] var newArray = arrayOfIntegers.filter {(value) -> Bool in return value % 2 == 0} print(newArray) /*打印:[2, 4, 6, 8]*/
filter 闭包也可以被简化,就像 map:
let arrayOfIntegers = [1,2,3,4,5,6,7,8,9] arrayOfIntegers.filter({(someInt:Int) -> Bool in return someInt % 2 == 0}) ///1.闭包语法 arrayOfIntegers.filter({(someInt:Int) in return someInt % 2 == 0}) ///2.省略返回值 arrayOfIntegers.filter({someInt in return someInt % 2 == 0}) ///3.省略参数指定类型 arrayOfIntegers.filter({$0 % 2 == 0}) ///4.省略return关键字 arrayOfIntegers.filter {$0 % 2 == 0} ///5.尾随闭包语法
-
Filter on dictionary
假设有一个书名当做 key ,书的价格当做 value 的字典。如果你想对这个字典调用 filter 函数,Swift 的自动补全将是这样:
filter 函数会调用一个名字为 isIncluded 的闭包,该闭包接受一个键值对作为参数,并返回一个布尔值。最终,基于返回的布尔值,filter 函数将决定是否将键值对添加到数组中。
<重要> 原文中作者写道:对字典调用 Filter 函数,将返回一个包含元组类型的数组。但译者在 playground 中发现 返回值实际为字典类型的数组。
let bookAmount = ["harrypotter":100.0,"junglebook":1000.0] let results = bookAmount.filter { (key,value) -> Bool in return value > 100} print(results) /*打印: ["junglebook": 1000.0] */
还可以将上述代码简化:
//$0 为 key $1 为 value let results = bookAmount.filter { $1 > 100 }
-
Filter on set
let lengthInMeters:Set = [1,2,3,4,5,6,7,8,9] let lengthInFeet = lengthInMeters.filter{$0 > 5} print(lengthInFeet) /*打印: [9, 8, 7, 6] */
在每次遍历时, filter 闭包接受一个 Double 的参数,返回一个布尔值。筛选数组中包含的元素基于返回的布尔值。
Reduce
reduce :联合集合中所有的值,并返回一个新值。即reduce 方法把数组元素组合计算为一个值,并且会接受一个初始值,这个初始值的类型可以和数组元素类型不同。
-
Apple 的官方文档如下:
func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) throws -> Result) rethrows -> Result
reduce 函数接受两个参数:
- 第一个为初始值,它用来存储初始值和每次迭代中的返回值。
- 另一个参数是一个闭包,闭包包含两个参数:初始值或者当前操作的结果、集合中的下一个 item 。
-
Reduce on arrays
让我们通过一个例子来理解 reduce 的具体作用:
let numbers = [1,2,3,4] let numberSum = numbers.reduce(0) { (x,y) in return (x + y) } print(numberSum) /*打印: 10 */
reduce 函数会迭代4次。
1.初始值为0,x为0,y为1 -> 返回 x + y 。所以初始值或者结果变为 1。
2.初始值或者结果变为 1,x为1,y为2 -> 返回 x + y 。所以初始值或者结果变为 3。
3.初始值或者结果变为 3,x为3,y为3 -> 返回 x + y 。所以初始值或者结果变为 6。
4.初始值或者结果变为 6,x为6,y为4 -> 返回 x + y 。所以初始值或者结果变为 10。
reduce 函数可以简化为:
let numbers = [1,2,3,4] let reducedNumberSum = numbers.reduce(0) { $0 + $1 } print(reducedNumberSum) // prints 10 /*打印: 10 */
在本例中,闭包的类型为
(Int,Int)->Int
。所以,我们可以传递类型为(Int,Int)->Int
的任意函数或者闭包。比如我们可以把操作符替换为 -, *, / 等。let reducedNumberSum = numbers.reduce(0,+) // returns 10
我们可以在闭包里添加
*
或者其他的操作符。let reducedNumberSum = numbers.reduce(0) { $0 * $1 } // reducedNumberSum is 0...
上面的代码也可以写成这样:
let reducedNumberSum = numbers.reduce(0,*)
reduce 也可以通过 + 操作符来合并字符串。
let codes = ["abc","def","ghi"] let text = codes.reduce("") { $0 + $1} //the result is "abcdefghi" or let text = codes.reduce("",+) //the result is "abcdefghi"
-
Reduce on dictionary
让我们来 reduce bookAmount。
let bookAmount = ["harrypotter":100.0, "junglebook":1000.0] let reduce1 = bookAmount.reduce(10) { (result, tuple) in return result + tuple.value } print(reduce1) //1110.0 /*打印: 1110.0 */ let reduce2 = bookAmount.reduce("book are ") { (result, tuple) in return result + tuple.key + " " } print(reduce2) //book are junglebook harrypotter /*打印: book are harrypotter junglebook */
对于字典,reduce 的闭包接受两个参数。
1.一个应该被 reduce 的初始值或结果
2.一个当前键值对的元组
reduce2 可以被简化为:
let reducedBookNamesOnDict = bookAmount.reduce("Books are ") { $0 + $1.key + " " } //or $0 + $1.0 + " "
-
Reduce on set
Set 中的 reduce 使用和数组中的一致。
let lengthInMeters: Set = [4.0, 6.2, 8.9] let reducedSet = lengthInMeters.reduce(0) { $0 + $1 } print(reducedSet) //19.1
闭包中的返回值类型为 Double。
Flatmap
Flatmap 用来铺平 collections 中的 collection 。在铺平 collection 之前,我们对每一个元素进行 map 操作。顾名思义FlatMap是将多个集合糅合成一个集合
Apple docs解释:返回一个对序列的每个元素进行形变的串级结果( Returns an array containing the concatenated results of calling the given transformation with each element of this sequence.)
解读 : map + (Flat the collection)
let codes = [["abc","def","ghi"],["abc","def","ghi"]] let newCodes = codes.flatMap{ $0.map{$0.uppercased()}} print(newCodes) /*打印:["ABC", "DEF", "GHI", "ABC", "DEF", "GHI"]*/
let codes = ["abc","def","ghi"] let newCodes = codes.flatMap{$0.uppercased()} print(newCodes) /*打印:["A", "B", "C", "D", "E", "F", "G", "H", "I"]*/
在上面代码中,flatMap 迭代 collections 中的所有 collection 进行大写操作。在这个例子中,每个 collection 是字符串。下面是执行步骤:
- 对所有的字符串执行
upperCased()
函数,这类似于:
[“abc”,”def”,”ghi”].map { $0.uppercased() }
[“ABC”, “DEF”, “GHI”]
- 将 collections 铺平为一个 collection。
output: ["A", "B", "C", "D", "E", "F", "G", "H", "I"]
let codes = "abhi muralidharan" let newCodes = codes.flatMap{return $0} print(newCodes) /*打印: ["a", "b", "h", "i", " ", "m", "u", "r", "a", "l", "i", "d", "h", "a", "r", "a", "n"] */
注意:在 Swift3 中,flatMap 还可以自动过滤 nil 值。但是现在已经废弃该功能。现在用 compactMap 来实现这一功能,稍后在文章中我们会讲到。
现在,你应该明白了 flatMap 是做什么得了。
-
Flatmap on array
let arrs = [[1,2,3], [4, 5, 6]] let flat1 = arrs.flatMap { return $0 } print(flat1) /*打印: [1, 2, 3, 4, 5, 6] */
-
Flatmap on array of dictionaries
因为在铺平之后的返回数组包含元素的类型为元组。所以我们不得不转换为字典。
let arrs = [["key1": 0, "key2": 1], ["key3": 3, "key4": 4]] let flat1 = arrs.flatMap { return $0 } print(flat1) /*打印: [(key: "key2", value: 1), (key: "key1", value: 0), (key: "key3", value: 3), (key: "key4", value: 4)] */ var dict = [String: Int]() flat1.forEach { (key, value) in dict[key] = value } print(dict) /*打印: ["key2": 1, "key3": 3, "key4": 4, "key1": 0] */
-
Flatmap on set
let numberSet:Set = [Set([4.0,6.2,8.9]),Set([9.9])] let flatmapSet = numberSet.flatMap{$0} print(flatmapSet) /*打印: [9.9, 4.0, 8.9, 6.2] */
-
Flatmap by filtering or mapping
我们可以用 flatMap 来实现将一个二维数组铺平为一维数组。 flatMap 的闭包接受一个集合类型的参数,在闭包中我们还可以进行 filter map reduce 等操作。
let collections = [[5, 2, 7], [4, 8], [9, 1, 3]] let onlyEven = collections.flatMap { (intArray) in intArray.filter({ $0 % 2 == 0}) } print(onlyEven) /*打印: [2, 4, 8] */
上述代码的简化版:
let collections = [[5, 2, 7], [4, 8], [9, 1, 3]] let onlyEven = collections.flatMap { $0.filter { $0 % 2 == 0 } } print(onlyEven) /*打印: [2, 4, 8] */
链式 : (map + filter + reduce)
我们可以链式调用高阶函数。 不要链接太多,不然执行效率会慢。下面的代码我在playground中就执行不了。
let arrayOfArrays = [[1, 2, 3, 4], [5, 6, 7, 8, 4]] let sumOfSquareOfEvenNums = arrayOfArrays.flatMap{$0}.filter{$0 % 2 == 0}.map{$0 * $0}.reduce {0, +} print(sumOfSquareOfEvenNums) // 136 //这样可以运行 let SquareOfEvenNums = arrayOfArrays.flatMap{$0}.filter{$0 % 2 == 0}.map{$0 * $0} let sum = SquareOfEvenNums.reduce(0 , +) // 136
CompactMap
在迭代完集合中的每个元素的映射操作后,返回一个非空的数组。
let arr = [1, nil, 3, 4, nil] let result = arr.compactMap{ $0 } print(result) /*打印: [1, 3, 4] */
它对于 Set ,和数组是一样的作用。
let nums: Set = [1, 2, nil] let r1 = nums.compactMap { $0 } print(r1) /*打印:[2, 1] */
而对于 Dictionary ,它是没有任何作用的,它只会返回一个元组类型的数组。所以我们需要使用 compactMapValues 函数。(该函数在Swift5发布)
let dict = ["key1": nil, "key2": 20] let result = dict.compactMap{ $0 } print(result) /*打印: [(key: "key1", value: nil), (key: "key2", value: Optional(20))]*/
let dict = ["key1": nil, "key2": 20] let result = dict.compactMapValues{ $0 } print(result) /*打印: ["key2": 20]*/
使用 map 是这样的。
let arr = [1, nil, 3, 4, nil] let result = arr.map { $0 } /*打印:[Optional(1), nil, Optional(3), Optional(4), nil] */
总结
以后在使用swift编码的过程中,当你意识到自己在对一个集合对象进行遍历操作时,你该思考一下是否可以使用map, filter或者reduce函数来替代。
以下是总结了以下map, filter, reduce的特性,
- 使用函数式编程不仅能减少代码的行数,还可使用链式结构构建复杂的逻辑。
- map返回了一个结果集,该集合中包含的所有元素是来于对源数组中每一个元素进行相同的转换之后形成的新元素。
当你需要映射一个数组,并且不需要改变返回数组的层级结构的时候,使用 map ,反之,则使用 flatMap 。
当返回数组中的值必须非空的时候,使用 compactMap ;当返回字典中的键值对中的value 必须为非空的时候,使用 compactMapValues 。
- filter返回一个结果集,该集合包含的元素是源数组中的每一个符合筛选条件的元素。
当你需要查询的时候,使用 filter 。
- reduce返回一个值,该值是对初始值和集合中的每个元素调用闭包中相同的操作生成的。
当你需要将数组进行某种计算并返回一个值得时候,使用 reduce 。