Swift5 高阶函数

map flatMap compactMap filter reduce sort

map(转换)

map(sequence, transformClosure): 如果transformClosure适用于所给序列中所有的元素,则返回一个新序列。

它接受一个闭包 transformClosure 作为参数,作用于数组中的每个元素,闭包返回一个变换后的元素,接着将所有这些变换后的元素组成一个新的数组。

// 计算字符串的长度
let stringArray = ["Objective-C", "Swift", "HTML", "CSS", "JavaScript"]
let counts = stringArray.map($0.count)
  • 适用于任何数据类型,能够返回不同与最初的类型,不能过滤掉不符合规则的元素
// 转化为 [Int?] 因为内部元素可能不符合转化规则
let strings = ["1", "2", "fish"]
let a = strings.map {Int($0)}
print(a)
//[Optional(1), Optional(2), nil]

compactMap(执行转换,解包所有可选选项并丢弃nil值)

与 map 作对比,转化成了[Int],并且过滤掉解包后为 nil 的值

// 转化为 [Int?] 因为内部元素可能不符合转化规则
let strings = ["1", "2", "fish"]
let a = strings.map {Int($0)}
let b = strings.compactMap {Int($0)}
print(a)
print(b)
// [Optional(1), Optional(2), nil]
// [1,2]

使用场景:

// 读出子视图中的 UIImageView
let imgViews = view.subviews.compactMap { $0 as? UIImageView }
// 读出有效的 URL
let urls = urlStrings.compactMap { URL(string: $0) }

flatMap(降低数组维度,降低可选项维度,过滤nil值)

思考下面的代码:

// name 是 String? 类型
let name: String? = nil  
// Int($0) 会将字符串转换为可选整数,因为字符串可能是非数字形式,例如“ Fish”。
// greeting 将是一个 Int?? 类型
let greeting = name.map { Int($0) }  

广义上讲,任何时候当你看到可选的可选内容时,某个地方就已经出现了问题,你应该重新考虑。
可选的可选意味着:

  • 外部可选项可能存在,而内部可选项可能存在.
  • 可能存在外部可选项,但内部可选项可能为nil。
  • 外部可选项可能为nil,这意味着没有内部可选项。

可选的可选参数使用起来非常混乱,但这就是 flatMap() 出现的地方:它不仅执行转换,随后将返回的内容展平,因此“可选的可选参数”变为“可选的”。
它会将双重可选值变为单个可选值。最终,我们不在乎外部可选或内部可选是否存在,仅在乎其中是否存在值,这就是为什么flatMap()如此有用的原因。

let number: String? = getUser(id: 97)
// 使用 flatMap 展平了一次,result 为 Int?
let result = number.flatMap { Int($0) }

与 map 的区别:

  • flatMap 会降低数组维度,过滤nil值,但是每次操作只执行一种功能,优先执行过滤 nil的操作。
  • 在没有明确指定原数组为:[Any]的情况下,会将 [String] 处理为 [String.Element],里面的元素由 String 降为 Character
  • 嵌套情况见下
let arr = [[[1,1,1],2],[3,4], nil, nil]
// 优先处理nil值
let arr1 = arr.flatMap{ $0 }
let arr2 = arr.flatMap{ $0 }.flatMap{ $0 }
print(arr1, arr2, separator: "\n")
// [[[1, 1, 1], 2], [3, 4]]
// [[1, 1, 1], 2, 3, 4]

使用效果对比

map(), flatMap() 和 compactMap() 的区别

总结:

  • map 和 compactMap 返回的元素不改变原先的类型
  • 在嵌套 map 的情况下,flatMap 会将 String 降维至 Character
  • 在不是嵌套 map 的情况下
    • 若原序列是 String 序列,不指定类型或指定为[String]:flatMap 将元素处理成 Character
    • 指定[Any]类型,flatMap 与 map、compactMap 一致,不改变原先类型。
  • flatMap 仅对 String 降为
let level = ["A", "B", "C"]
let map = level.map{$0}
let flatMap = level.flatMap{$0}
let compactMap = level.compactMap{$0}
let level = ["A", "B", "C"]
let num = [1, 2]
let result = level.??? { (level) in
    num.??? { (num) in
        return level + String(num)
    }
}

作用于可选对象

可选的map和flatMap的用法

map()方法也存在于可选对象上:如果一个可选类型有值,map会获取这个值,经过map的闭包处理变为另外一个值,如果这个可选类型的值为nil,那么不会执行map闭包,而是直接返回nil。

// 如果name包含字符串,则map()会将解包,将其转换为“ Hi, name包含的字符串”,然后将整个拼接后的字符串放入一个可选对象中并返回以存储在greeting中。
// 如果name不包含字符串为nil,map()将直接返回nil给greeting。
let name: String? = valueOrNil(id: 97)
let greeting = name.map { "Hi, \($0)" }
print(greeting ?? "Unknown user")

类似的 flatMap() 同样可以作用于可选对象,区别是map闭包不能return nil,而flatmap可以

var value: Double? = 10
var newValue: Double? = value.flatMap { v in
    if v < 5.0 {
        return nil
    }
    return v / 5.0
}
// newValue is now Optional(2)

newValue = newValue.flatMap { v in
    if v < 5.0 {
        return nil
    }
    return v / 5.0
}
// now it's nil

使用场景:替代三目运算符

一般在使用可选对象时,必须分有值和nil值的情况,运用到三目运算符时肯定会使用强制解包,这样很不优雅。
使用 map 代替,如果可选值为 nil,则会直接返回 nil,不会进入后面的闭包。

// 时间操作
var date: NSDate? = ...
// var formatted: String? = date == nil ? nil : NSDateFormatter().stringFromDate(date!)
var formatted: String? =  date.map(NSDateFormatter().stringFromDate)

// 获取String字面量的值
func ageToString(age: Int?) -> String {
    //return age == nil ? "Unknown age" : "She is (age!) years old"
    return age.map { "She is ($0) years old" } ?? "Unknown age"
}

// 本地化字符串
let label = UILabel()
func updateLabel(value: String?) {
  label.text = value.map { 
    String.localizedStringWithFormat(NSLocalizedString("value %@", comment: ""), $0) 
  }
}

// 构造枚举
enum State: String {
    case Default = ""
    case Cancelled = "CANCELLED"

    static func parseState(state: String?) -> State {
        return state.flatMap(State.init) ?? .Default
    }
}

// 在数组中找到某项
func find(identifier: String) -> Item? {
    return items.indexOf({$0.identifier == identifier}).map({items[$0]})
}

// json字典构造对象
func createPerson(json: [String: AnyObject]) -> Person? {
    return (json["person"] as? [String: AnyObject]).flatMap(Person.init)
}

filter(过滤)

可以对数组中的元素按照某种规则进行一次过滤

let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9]
//筛选偶数值
let evens = numbers.filter{$0 % 2 == 0} //等同于
let even = numbers.filter{(num) -> Bool in
      num % 2 == 0
}

//筛选奇数值
let odds = numbers.filter{$0 % 2 == 1} //等同于
let odd = numbers.filter{(num) -> Bool in
      num % 2 == 1
}

reduce(归纳合并成一个元素)

Swift化零为整:Reduce方法详解

Reduce 的基础思想是将一个序列转换为一个不同类型的数据,期间通过一个累加器(Accumulator)来持续记录递增状态。

  • reduce(initial, combineClosure): 从第一个初始值开始对其进行combineClosure操作,递归式地将序列中的元素合并为一个元素

  • combineClosure:规则闭包,要返回如何将元素合并,有两个参数

    • $0 代表累加器,初值等于 initial
    • $1 代表遍历数组得到的一个元素
var languages = ["Swift", "OC", "java"]
let r = languages.reduce("_", {$0 + $1}) //等同于
let r = languages.reduce("_") { (result, next) -> String in
    print("result: \(result)")
    print("next: \(next)")
    return result + next
}
print(r)

/**
result: _
next: Swift
result: _Swift
next: OC
result: _SwiftOC
next: java
_SwiftOCjava
*/

print([10, 20, 5].reduce(1, { $0 * $1 }) == 1000) // true
// 极简形式 [10, 20, 5].reduce(1, *)

值的注意的问题

  • 仅传入计算符号 "+" "*" 作为一个 combinator 函数是有效的
    • 它仅仅是对 lhs(Left-hand side,等式左侧) 和 rhs(Right-hand side,等式右侧) 做计算处理,最后返回结果值
[0, 1, 2, 3, 4].reduce(0, +)
[1, 2, 3, 4].reduce(1, *)
  • 反转数组:
[1, 2, 3, 4, 5].reduce([Int](), { [$1] + $0 })
// [element]:[1] result:[] 
// [element]:[2] result:[1] 
// [element]:[3] result:[2, 1] 
// [element]:[4] result:[3, 2, 1] 
// [element]:[5] result:[4, 3, 2, 1] 
  • 性能问题:
    使用高阶函数之前多考虑实现方案,通常情况下,map 和 filter 所组成的链式结构会引入性能上的问题,因为它们需要多次遍历你的集合才能最终得到结果值,这种操作往往伴随性能损失。
// 初始序列(即 [0,1,2,3,4])被重复访问了三次之多
[0, 1, 2, 3, 4].map({ $0 + 3}).filter({ $0 % 2 == 0}).reduce(0, combine: +)

// 可用 reduce 完全替换实现,极大提高执行效率:
[0, 1, 2, 3, 4].reduce(0, { (ac, r) in 
  if (r + 3) % 2 == 0 {
   return ac + r + 3
  } else {
   return ac
  }
})

// for-loop 版本
var ux = 0
for i in 0...100000 {
    if (i + 3) % 2 == 0 {
      ux += (i + 3)
    }
}

不过,在某些情况中,链式操作是优于 reduce 的。思考如下范例:

[0...100000].map({ $0 + 3}).reverse().prefix(3)
// 0.027 Seconds
[0...100000].reduce([], combine: { (var ac: [Int], r: Int) -> [Int] in
    ac.insert(r + 3, atIndex: 0)
    return ac
}).prefix(3)
// 2.927 Seconds

使用链式操作花费 0.027s,这与 reduce 操作的 2.927s 形成了鲜明的反差,从 reduce 的语义上来说,传入闭包的参数(如果可变的话,即 mutated),会对底层序列的每个元素都产生一份 copy 。在我们的案例中,这意味着 accumulator 参数 ac 将为 0...100000 范围内的每个元素都执行一次复制操作。Arrays, Linked Lists and Performance
因此,当我们试图使用 reduce 来替换掉一组操作时,请时刻保持清醒,问问自己:reduction 在问题中的情形下是否确实是最合适的方式。

更多范式

更多:Swift化零为整:Reduce方法详解

  • min:返回列表中的最小项。[1,5,2,9,4].minElement() 方法更胜一筹。
// 初始值为 Int.max,传入闭包为 min:求两个数的最小值
// min 闭包传入两个参数:1. 初始值 2. 遍历列表时的当前元素
// 倘若当前元素小于初始值,初始值就会替换成当前元素
// 示意写法: initial = min(initial, elem)
[1, 5, 2, 9, 4].reduce(Int.max, min)
  • unique:剔除列表中重复的元素。最好的解决方式是使用集合(Set)
[1, 2, 5, 1, 7].reduce([], { (a: [Int], b: Int) -> [Int] in
if a.contains(b) {
   return a
} else {
   return a + [b]
}
})
// prints: 1, 2, 5, 7

sort

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