map、filter、reduce的用法

Swift中的mapfilterreduce可以对Array、Dictionary等集合进行操作。如果你没有函数式编程经验,你可能更习惯于使用for-in遍历。这一篇文章将介绍mapfilterreduceflatMapcompactMap的用法。

Swift中的mapreducefilter函数来自于函数式编程(functional programming),被称为高阶函数(high-order function)。高阶函数是至少满足下列一个条件的函数:

  • 接受一个或多个函数作为输入
  • 输出一个函数

在数学中,也叫做算子。微积分中的导数就是常见的例子,它会映射一个函数到另一个函数。

1. Map

func map<T>(_ transform: (Self.Element) throws -> T) rethrows -> [T]

map用于遍历序列(sequence),并对每个元素执行相同操作。map函数对所有元素执行完映射或转换后,返回一个包含所有元素的数组,元素类型可以是原来的类型,也可以是新类型。

ForInMap.png

我们可以使用for-in循环计算数组元素的平方,如下:

let values = [2.0, 4.0, 5.0, 7.0]
var squares: [Double] = []
for value in values {
    squares.append(value*value)
}

for-in可以解决问题,但代码有些冗余。需要声明指定类型的数组,以便在循环时添加元素;数组还需为变量。使用map实现如下:

let squares2 = values.map {$0 * $0}

for-in相比,map有了很大的提升。squares2是一个常量,Swift可以自动推断其类型。

刚接触简写的闭包语法时,可能不易理解。map函数只有一个参数,即尾随闭包(一个函数)。map遍历集合元素时调用闭包,闭包接收传入的元素,并返回一个结果。map函数以数组的形式返回最终结果。

map函数完整格式如下:

let squares3 = values.map({
    (value: Double) -> Double in
    return value * value
})

闭包包含一个参数:(value: Double),返回一个Double类型的值,Swift的自动推断可以推断出这些。由于map只包含一个参数,且是闭包,( )也可以省略。闭包内只有一行代码时,return也可以省略。更新后如下:

let squares4 = values.map {value in value * value}

in关键字将闭包的参数和主体分开,这里可以进一步将参数省略,使用带编号的参数:

let squares5 = values.map { $0 * $0 }

map返回数组数据类型可以和原数组不一致。下面将数值类型转换为字符串类型:

let scores = [0, 28, 648]
let words = scores.map { NumberFormatter.localizedString(from: $0 as NSNumber, number: .spellOut) }
// ["zero", "twenty-eight", "six hundred forty-eight"]

map函数除了用于数组,还可用于其他集合类型,例如字典、Set,但返回结果永远是数组。如下所示:

let milesToPoint = ["point1":120.0,"point2":50.0,"point3":70.0]
let kmToPoint = milesToPoint.map { $1 * 1.6093 }
// [193.11599999999999, 80.465, 112.651]

上面遍历字典时,闭包的两个参数分别为key和value。如果无法区分出参数类型,可以查看Xcode自动补全的提示:

ForInArgumentType.png

2. Filter

func filter(_ isIncluded: (Self.Element) throws -> Bool) rethrows -> [Self.Element]

filter函数返回符合指定条件的有序数组。isIncluded是一个闭包,接收序列的元素,返回Bool类型值,以指示该元素是否应包含在返回的数组中。filter复杂度为O(n),n为序列的长度。

ForInFilter.png

filter函数只包含一个参数,即闭包。在闭包内添加需满足的条件,返回值为Bool类型。true表示会把元素添加到结果的数组中;false表示不会添加。

下面使用filter过滤奇数,只把偶数添加到数组中:

let digits = [1,4,10,15]
let even = digits.filter { $0 % 2 == 0 }
// [4, 10]

3. Reduce

reduce将集合中的所有元素合并为一个新的值。

ForInReduce.png

reduce函数声明如下:

func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) throws -> Result) rethrows -> Result
  • initialResult:作为初始值,首次调用闭包时传递给nextPartialResult
  • nextPartialResult:闭包内将已经组合的值与新的元素合并,并在下一次调用nextPartialResult闭包时使用,最后返回给调用方。如果集合没有元素,返回结果为initialResult

reduce(_:_:)复杂度为O(n)

下面将数组元素值与初始值10相加:

let items = [2.0, 4.0, 5.0, 7.0]
let total = items.reduce(10.0) { partialResult, value in
    partialResult + value
}
// 28.0

闭包可以进一步简化:

let total = items.reduce(10.0, +)

reduce也可用于拼接数组中的字符串:

let codes = ["abc","def","ghi"]
let text = codes.reduce("1", +)
1abcdefghi

4. FlatMap和CompactMap

flatMapcompactMapmap的变体,适用于以下三种场景:

4.1 flatMap用于处理序列,并返回序列

Sequence.flatMap<S>(_ transform: (Element) -> S)
    -> [S.Element] where S : Sequence

序列调用flatMap后,每个元素都会执行闭包逻辑,并返回 flatten 结果:

let results = [[5,2,7], [4,8], [9,1,3]]
let allResults = results.flatMap { $0 }
// [5, 2, 7, 4, 8, 9, 1, 3]

let passMarks = results.flatMap { $0.filter { $0 > 5} }
// [7, 8, 9]

4.2 flatMap处理可选项

闭包接收可选类型中的非nil值,返回可选类型:

Optional.flatMap<U>(_ transform: (Wrapped) -> U?) -> U?

如果可选类型为nilflatMap也会返回nil

let input: Int? = Int("8")
let passMark: Int? = input.flatMap { $0 > 5 ? $0 : nil }
// Optional(8)

4.3 compactMap处理序列,返回可选类型

Sequence.compactMap<U>(_ transform: (Element) -> U?) -> U?

这种用途的flatmap在Swift 4.1(Xcode 9.3)被重命名为compactMap。为移除数组中的nil元素提供了一种简便操作:

let keys: [String?] = ["Tom", nil, "Peter", nil, "Harry"]
let validNames = keys.compactMap { $0 }
validNames
// ["Tom", "Peter", "Harry"]

let counts = keys.compactMap { $0?.count }
counts
// [3, 5, 5]

5. 链式使用

可以将mapfilterreduce组合使用。例如,计算数组中元素值大于等于5之和,可以先filter,再reduce。如下所示:

let marks = [6, 4, 8, 2, 9, 7]
let totalPass = marks.filter{$0 >= 5}.reduce(0, +)
// 30

计算数组元素值平方,并返回偶数。因为奇数的平方仍然是奇数,可以先过滤掉奇数,再做转换:

let numbers = [648, 17, 35, 4, 12]
let evenSquares = numbers.filter{$0 % 2 == 0}.map{$0 * $0}
// [419904, 16, 144]

总结

以后遇到for-in遍历集合时,可以考虑是否可以使用mapfilterreduce替换:

  • map:将sequence元素转换后,以数组形式返回。
  • filter:只有满足条件的元素会被放到数组返回。
  • reduce:每个元素都会调用 combine closure,首次调用时与initialResult组合。

参考资料:

  1. Swift Guide to Map Filter Reduce
  2. Map, Reduce and Filter in Swift

欢迎更多指正:https://github.com/pro648/tips

本文地址:https://github.com/pro648/tips/blob/master/sources/map、filter、reduce的用法.md

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

推荐阅读更多精彩内容