Swift 高阶函数


高阶函数的定义:

Wikipedia 中,是这么定义高阶函数(higher-order function)的,如果一个函数:

  • 接收一个或多个函数当作参数

  • 把一个函数当作返回值

至少满足以上条件中的一个的函数,那么这个函数就被称作高阶函数。

使用高阶函数进行函数式编程的优势

  • 简化代码

  • 使逻辑更加清晰

  • 当数据比较大的时候,高阶函数会比传统实现更快,因为它可以并行执行(如运行在多核上)

高阶函数在Swift语言中有大量的使用场景,本篇分析 Swift 提供的如下几个高阶函数:mapflatMapcompactMapfilterreduce


一、map

map方法获取一个闭包表达式作为其唯一参数。

数组中的每一个元素调用一次该闭包函数,并返回该元素所映射的值。

简单说就是数组中每个元素通过某种规则(闭包实现)进行转换,最后返回一个新的数组。

1. 基本使用

a. 需求:将Int类型数组中的元素乘以2,然后转换为String类型的数组


let ints = [1, 2, 3, 4]

let strs = ints.map { "\($0 * 2)" }

// 打印结果:["2", "4", "6", "8"]

print(strs)

b. 需求:生成一个新的Int数组,元素是多少元素就重复多少个


let nums = [1, 2, 3, 4]

let mapNums = nums.map { Array(repeating: $0, count: $0) }

// 打印结果:[[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]

print(mapNums)

最终返回的是一个二维数组。

c. 需求:将String类型的数组转换为Int类型的数组


let someAry = ["12", "ad", "33", "cc", "22"]

// var nilAry: [Int?]

let nilAry = someAry.map { Int($0) }

// 打印结果:[Optional(12), nil, Optional(33), nil, Optional(22)]

print(nilAry)

最终返回的是[Int?],一个可选类型的Int数组,且元素中存在nil

2. 源码分析

由于Swfit是开源的,所以我们可以通过源码来分析map具体做了些什么。

源码:https://github.com/apple/swift/blob/master/stdlib/public/core/Collection.swift


public func map<T>(

    _ transform: (Element) throws -> T

  ) rethrows -> [T] {

    // TODO: swift-3-indexing-model - review the following

    let n = self.count

    if n == 0 {

      return []

    }

    var result = ContiguousArray<T>()

    result.reserveCapacity(n)

    var i = self.startIndex

    for _ in 0..<n {

      result.append(try transform(self[i]))

      formIndex(after: &i)

    }

    _expectEnd(of: self, is: i)

    return Array(result)

}

对于这个代码,我们可以看出,它做了以下几件事情:

1. 构造一个名为 result 且与原数组的 capacity 一致的新数组,用于存放新的结果;

2. 遍历自己的元素,对于每个元素,调用闭包的转换函数 transform ,进行转换;

3. 将转换的结果使用 append 方法放入 result 中;

4. 遍历完成后,返回 result 。

tips: ContiguousArraySwift提供的更高性能的数组,几乎与Array没什么区别,如果不涉及Objective-C的混编或需要转NSArray,完全可以使用ContiguousArray取代Array来使用,可以有更高的性能。

至于与Array的区别,这里就不拓展了,有兴趣的小伙伴Google一下。

二、flatMap


// flatMap 定义

public func flatMap<SegmentOfResult>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Element] where SegmentOfResult : Sequence

// Swift 4.1 以前的定义,4.1之后改名为 compactMap,compactMap时会详细说明

@available(swift, deprecated: 4.1, renamed: "compactMap(_:)", message: "Please use compactMap(_:) for the case where closure returns an optional value")

public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]

flatMap的实现与map非常类似,也是数组中每个元素通过某种规则(闭包实现)进行转换,最后返回一个新的数组。

不过flatMap能把数组中存有数组的数组(二维数组、N维数组)一同打开变成一个新的数组,称为降维,通俗一点就是把多维数组都会拍扁为一维数组,通过后面的例子一看就明白了。

flatMap另一个解包的功能在 4.1 版本之后更名为compactMap,所以在compactMap再做说明。

1. 基本使用

a. 需求:将Int类型数组中的元素乘以2,然后转换为String类型的数组


let ints = [1, 2, 3, 4]

let strs = ints.flatMap { "\($0 * 2)" }

// flatMap打印结果:["2", "4", "6", "8"]

print(strs)

该例子使用的与map一样,结果也是一样的,普通情况下,两者的效果一致。

b. 需求:生成一个新的Int数组,元素是多少元素就重复多少个


let nums = [1, 2, 3, 4]

let mapNums = nums.flatMap { Array(repeating: $0, count: $0) }

// flatMap打印结果:[1, 2, 2, 3, 3, 3, 4, 4, 4, 4]

print(mapNums)

可以看到,flatMap把数组中的数组都打开了,最终返回的是一个一维数组。而map返回的是一个二维数组,没有降维。接下来通过源码,来分析两者的差别。

2. 源码分析

源码:https://github.com/apple/swift/blob/master/stdlib/public/core/SequenceAlgorithms.swift


public func flatMap<SegmentOfResult: Sequence>(

    _ transform: (Element) throws -> SegmentOfResult

  ) rethrows -> [SegmentOfResult.Element] {

    var result: [SegmentOfResult.Element] = []

    for element in self {

      result.append(contentsOf: try transform(element))

    }

    return result

}

我们可以看出,它做了以下几件事情:

1. 构造一个名为 result 的新数组,用于存放新的结果;

2. 遍历自己的元素,对于每个元素,调用闭包的转换函数 transform ,进行转换;

3. 将转换的结果使用 append-contentsOf 方法放入 result 中;

4. 遍历完成后,返回 result 。

仔细观察,flatMapmap是有一些区别的:

A. transform的差别

  • maptransform接收的参数是数组元素然后输出的是闭包执行后的类型T,最终执行的结果的是[T]

  • flatMaptransform接收的参数是数组的元素,但输出的一个Sequence类型,最终执行的结果并不是Sequence的数组,而是Sequence内部元素另外组成的数组,即:[Sequence.Element]

B. 第三个步骤的差别

  • map使用append方法放入result中,所以transform之后的结果是什么类型,就将什么类型放入result中;

  • flatMap使用append-contentsOf方法放入result中,而appendContentsOf方法就是把Sequence中的元素一一取出来,然后再放入result中,这也就是flatMap能降维的原因。

tips: append-contentsOf方法是干什么用的,看一段代码就明白了:


// 定义:

public mutating func append<S>(contentsOf newElements: __owned S) where Element == S.Element, S : Sequence

var arrCons = [1, 3, 2]

arrCons.append(contentsOf: [4, 5])

// 打印结果:[1, 3, 2, 4, 5]

print(arrCons)

也就是会把目标Sequence的元素一一取出来,然后放入定义好的数组。

所以flatMap必须要求 transform 函数返回的是一个Sequence类型,因为append-contentsOf方法需要的是一个Sequence类型的参数。

Sequence是什么呢 ?Sequence是一个协议,主要有两个参数,一个是Element,也即是 Sequence里的元素,另一个则是Iterator(迭代器),且自身的Element与迭代器的Element是要相同的where Self.Element == Self.Iterator.Element。迭代器是遵循IteratorProtocol协议的,而IteratorProtocol的核心是next()方法,这个方法在每次被调用时返回序列中的下一个值,对Sequence的遍历,实际上调用的就是迭代器的next()方法,当序列下一个值为空时,next()返回nil,也就意味着遍历结束。Sequence的高阶函数都是在Sequence的扩展中定义的。

这里就不拓展了,有兴趣的小伙伴Google以及查看官方源码。

说到这里,我们就可以解释如下代码了:


let nums = [1, 2, 3, 4]

let mapNums = nums.map { Array(repeating: $0, count: $0) }

let flatMapNums = nums.flatMap { Array(repeating: $0, count: $0) }

// map打印结果:[[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]

print(mapNums)

// flatMap打印结果:[1, 2, 2, 3, 3, 3, 4, 4, 4, 4]

print(flatMapNums)

transform执行的结果都是一样的,都是得到一个数组,差别在于map将数组直接放入result中,而flatMap将数组中的元素一一取出来,更准确的说是调用Sequence的迭代器next()方法,将元素一一取出来,然后再放入result中。

然后说说第一个例子,用mapflatMap的效果是一样的,是因为flatMap闭包执行后输出的Sequence是一个String类型,与用maptransform输出的类型是一致的,而String是遵循Sequence协议的并且只有一位Character,所以结果是一样的,如果我们转换的是多位的String,会是什么效果呢?

看如下的代码:


let ints = [1, 2, 3, 4]

let mapStrs = ints.map { "\($0 * 2)啥啥啥" }

// map打印结果:["2啥啥啥", "4啥啥啥", "6啥啥啥", "8啥啥啥"]

print(mapStrs)

let flatMapStrs = ints.flatMap { "\($0 * 2)啥啥啥" }

// flatMap打印结果:["2", "啥", "啥", "啥", "4", "啥", "啥", "啥", "6", "啥", "啥", "啥", "8", "啥", "啥", "啥"]

print(flatMapStrs)

// String 的迭代器测试,这也就解释了flatMap在调用`append-contentsOf`方法是,是将 transform 后得到的 String 的 每一个 Character 放入了result中

var testStr = "test"

var iterator = testStr.makeIterator()

// Optional("t")

print(iterator.next())

// Optional("e")

print(iterator.next())

// Optional("s")

print(iterator.next())

// Optional("t")

print(iterator.next())

// nil

print(iterator.next())

transform执行的结果都是一样的,都是得到一个String,差别在于mapString直接放入result中,而flatMapString中的Character一一取出来,更准确的说是调用String的迭代器next()方法,将Character一一取出来,然后再放入result中。

三、Optional 中的 map 和 flatMap

上面所分析的都是数组的mapflatMap,不只是数组中有这两个高阶函数,Optional有这两个高阶函数。

先看看Optional中的定义:


public enum Optional<Wrapped> : ExpressibleByNilLiteral {



    case none

    case some(Wrapped)



    public init(_ some: Wrapped)

    public func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?

    public func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?

}

1. 基本使用

a. 需求:修改一个可选Int的值


let a1: Int? = 1

let b1 = a1.map { $0 * 2 }

// 打印结果:Optional(2)

print(b1)

let a2: Int? = nil

let b2 = a2.map { $0 * 2 }

// 打印结果:nil

print(b2)

通过例子,说明对于一个Optional的变量来说,map方法允许它再次修改自己的值,并且不必关心自己是否为.None

b. 需求:对NSDate做format操作


// 不使用 map 的写法

let date: Date? = Date()

let formatter = DateFormatter()

formatter.dateFormat = "YYYY-MM-dd"

var formatted: String? = nil

if let d = date {

    formatted = formatter.string(from: d)

}

// 打印结果:Optional("2019-07-16")

print(formatted)

// 使用 map 函数后,代码变得更短,更易读:

let date: Date? = Date()

let formatter = DateFormatter()

formatter.dateFormat = "YYYY-MM-dd"

let formatted = date.map { formatter.string(from: $0) }

// Optional("2019-07-24")

print(formatted)

通过例子,我们可以得出结论,当输入的是一个Optional,同时需要在逻辑中处理这个Optional是否为nil,那么就适合用map来替代原来的写法,使得代码更加简短。

c. 需求:将一个字符串转换成Int


let s: String? = "abc"

let mapR = s.map { Int($0) }

let flatMapR = s.flatMap { Int($0) }

// Optional(nil) --> map 会多包一层Optional

print(mapR)

// nil

print(flatMapR)

从上面的例子,我们可以得出结论,当我们的闭包参数有可能返回nil的时候,使用flatMap会更加合适,map会多包一层Optional,这样就很容易导致多重Optional嵌套的问题。

什么是多重Optional嵌套呢,Optionalmap使用不当,就会导致多重Optional嵌套的问题。

我们来看一段代码:


let tq: Int? = 1

// let b: Int??

let b = tq.map { (a: Int) -> Int? in

    if a % 2 == 0 {

        return a

    } else {

        return nil

    }

}

// 打印结果:"b is not nil"

if let _ = b {

    print("b is not nil")

} else {

    print("b is nil")

}

由上面的代码就是Optionalmap使用不当而导致的多重Optional嵌套,多重Optionalnilif-let的判定是失效的,所以在工作中尽量避免多重Optional嵌套问题。上面例子的解决办法就是将map替换成flatMap,由于flatMap会有一次解包操作,所以能避免多重Optional嵌套的问题。

关于多重Optional嵌套的问题,这里就不拓展了,有兴趣的小伙伴可以看看这里:

http://blog.devtang.com/2016/02/27/swift-gym-1-nested-optional/

2. 源码分析

源码:https://github.com/apple/swift/blob/master/stdlib/public/core/Optional.swift


@inlinable

  public func map<U>(

    _ transform: (Wrapped) throws -> U

  ) rethrows -> U? {

    switch self {

    case .some(let y):

      return .some(try transform(y))

    case .none:

      return .none

    }

  }



  @inlinable

  public func flatMap<U>(

    _ transform: (Wrapped) throws -> U?

  ) rethrows -> U? {

    switch self {

    case .some(let y):

      return try transform(y)

    case .none:

      return .none

    }

  }

这两函数是惊人的相似,不仔细看的话,甚至看不出这两个函数的差别,两函数实现当然是差别的:

1. map 返回的是 U ,flatMap 返回的是 U?

2. map 把结果放到 .Some 里面返回,也就是会调用一次 Optional 的构造函数,多包一层 Optional ,flatMap把结果直接返回

两个函数最终都保证了返回结果是Optional的,只是将结果转换成Optional的位置不一样。

既然OptionalmapflatMap本质上是一样的,为什么要搞两种形式呢?这其实是为了调用者更方便而设计的。调用者提供的闭包函数,既可以返回Optional的结果,也可以返回非Optional的结果。对于后者,使用map方法,即可以将结果继续转换成Optional的。结果是Optional的意味着我们可以继续链式调用,也更方便我们处理错误。

最后我们来看一段代码:


var arr = [1, 2, 4]

let res = arr.first.flatMap {

    arr.reduce($0, max)

}

// 打印结果:Optional(4)

print(res)

代码的意思是:计算出数组中的元素最大值,按理说,求最大值直接使用reduce方法就可以了,不过有一种特殊情况需要考虑,即数组中的元素个数为 0 的情况,在这种情况下,没有最大值。

所以这里使用OptionalflatMap方法来处理了这种情况。arrfirst方法返回的结果是Optional的,当数组为空的时候,first方法返回.None,所以,这段代码可以处理数组元素个数为 0 的情况了。


var arr: [Int] = []

let res = arr.first.flatMap {

    arr.reduce($0, max)

}

// nil

print(res)

四、compactMap

compactMap是在4.1之后对flatMap的一个重载方法的重命名,同样是数组中每个元素通过某种规则(闭包实现)进行转换,最后返回一个新的数组,不过compactMap会将nil剔除,并对Optional进行解包。

1. 基本使用

a. 需求:将String类型的数组转换为Int类型的数组


var someAry = ["12", "ad", "33", "cc", "22"]

// var compactMapAry: [Int]

var compactMapAry = someAry.compactMap { Int($0) }

// compactMap打印结果:[12, 33, 22]

print(compactMapAry)

最终返回的是[Int],一个Int数组,并将其中转换失败的nil过滤掉了,并对转换成功的Optional值进行了解包。

2. 源码分析

源码:https://github.com/apple/swift/blob/master/stdlib/public/core/SequenceAlgorithms.swift


public func compactMap<ElementOfResult>(

    _ transform: (Element) throws -> ElementOfResult?

  ) rethrows -> [ElementOfResult] {

    return try _compactMap(transform)

}

public func _compactMap<ElementOfResult>(

_ transform: (Element) throws -> ElementOfResult?

) rethrows -> [ElementOfResult] {

    var result: [ElementOfResult] = []

    for element in self {

      if let newElement = try transform(element) {

        result.append(newElement)

      }

    }

    return result

}

通过代码,我们可以看出,它做了以下几件事情:

1. 调用 _compactMap 方法 传入 transform;

2. 构造一个名为 result 的新数组,用于存放新的结果;

3. 遍历自己的元素,对于每个元素,调用闭包的转换函数 transform ,进行转换;

4. 将转换的结果 使用 if - let 后,再使用 append 方法放入 result 中;

5. 遍历完成后,返回 result 。

从这里就可以得出结论,compactMapmap的区别就在于,maptransform后的结果直接放入result中,而compactMap使用if-let后再放入result中,而if-let的作用就是解包和过滤nil

在看如下代码,其中的差别以及为什么会有差别,就很清晰了。


var someAry = ["12", "ad", "33", "cc", "22"]

var mapAry = someAry.map { Int($0) }

var compactMapAry = someAry.compactMap { Int($0) }

// map打印结果:[Optional(12), nil, Optional(33), nil, Optional(22)]

print(mapAry)

// compactMap打印结果:[12, 33, 22]

print(compactMapAry)

五、filter

filter用来过滤元素,即筛选出数组元素中满足某种条件(闭包实现)的元素。

1. 基本使用

a. 需求:筛选出Int数组中的偶数


let nums = [1, 13, 12, 36, 77, 89, 96]

let result = nums.filter { $0 % 2 == 0 }

// 打印结果:[12, 36, 96]

print(result)

最终返回的是全部是偶数的Int数组。

2. 源码分析

源码:https://github.com/apple/swift/blob/master/stdlib/public/core/Sequence.swift


public __consuming func filter(

    _ isIncluded: (Element) throws -> Bool

  ) rethrows -> [Element] {

    return try _filter(isIncluded)

  }

  @_transparent

public func _filter(

    _ isIncluded: (Element) throws -> Bool

  ) rethrows -> [Element] {

    var result = ContiguousArray<Element>()

    var iterator = self.makeIterator()

    while let element = iterator.next() {

      if try isIncluded(element) {

        result.append(element)

      }

    }

    return Array(result)

}

通过代码,我们可以看出,它做了以下几件事情:

1. 调用 _filter 方法 传入 isIncluded ;

2. 构造一个名为 result 的新数组,用于存放新的结果;

3. 使用迭代器,遍历所有的元素,对于每个元素,调用闭包 isIncluded ,判断是否符合条件;

4. 将符合条件的元素使用 append 方法放入 result 中;

5. 遍历完成后,返回 result 。

六、reduce

reduce有两个函数,先看看定义:


@inlinable public func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) throws -> Result) rethrows -> Result

@inlinable public func reduce<Result>(into initialResult: __owned Result, _ updateAccumulatingResult: (inout Result, Element) throws -> ()) rethrows -> Result

reduce两个函数都是把数组元素组合计算为另一个值,并且会接受一个初始值,这个初始值的类型可以和数组元素类型不同,这样子就有很大的可操作空间,也会很有趣,后面会说到。

reduce两个函数效果是一样的,但也是有差别的,差别就在于闭包的定义:

  • 第一个函数闭包,接收ResultElement,返回闭包执行后的Result,后续的操作是将每次闭包执行后的Result当做下一个元素执行闭包的入参,直到遍历完所有元素;

  • 第二个函数闭包,接收的依然是ResultElement,不过没有返回值,并且Result是用inout修饰的,所以传入闭包的是Result的地址,所以闭包的执行都是基于Result进行操作的,这么说可能有些抽象,下面的源码分析,一看就明白了。

还有一个区别就是初始值,第二个函数使用了__owned进行了修饰,这个我也没懂是为什么,如果有知道的小伙伴望不吝赐教。

这两个函数实现的效果是一样的,只是实现的方式不同而已,第一个函数能做到的事情,第二个函数都能做到。

1. 基本使用

a. 需求:求一个Int类型数组的和


var arr = [2, 3, 4, 5]

// 正经写法

//let r = arr.reduce(0) { $0 + $1 }

// 简写

let r = arr.reduce(0, +)

// 打印结果:14

print(r)

let r2 = arr.reduce(into: 0, +=)

// 打印结果:14

print(r2)

最终返回的 14,即 0 + 2 + 3 + 4 + 5 的和,其中0是初始值。除了能获取和,当然也能能获取积、商和差。let r = arr.reduce(0, +) 简写中的符号,可以使用 + - * /,在/的时候注意除数不能为0,可以修改试试看。

这里能够简写的原因是由于Swift强大的类型推导,闭包仅仅传入了一个+号,Swift推导过程是首先nextPartialResult闭包有两个传入参数ResultElement,除此之外别无其他,因此+只能对这两个参数求和,得到一个结果值称为x吧,由于nextPartialResult函数还需要返回一个结果值,但是除了x也没有其他的可能,因此把x作为闭包结果值返回和Result进行相加计算,然后返回。

b. 需求:求出数组中奇数的和、以及偶数乘积


let nums = [1, 3, 2, 4]

typealias ResTuple = (Int, Int)

let res = nums.reduce((0, 1)) { (r, i) -> ResTuple in

    var temp = r

    if i % 2 == 0 {

        temp.1 *= i

    } else {

        temp.0 += i

    }

    return temp

}

// 打印结果:奇数和:4,偶数乘积:8

print("奇数和:\(res.0),偶数乘积:\(res.1)")

let res1 = nums

    .reduce(into: (0, 1)) { $1 % 2 == 0 ? ($0.1 *= $1) : ($0.0 += $1) }

// 打印结果:reduce-into --> 奇数和:4,偶数乘积:8

print("reduce-into --> 奇数和:\(res1.0),偶数乘积:\(res1.1)")

通过这个例子,reduce能做的不仅仅是求和或拼接,还可以做更多个性化的事情。上面实现中最有意思的莫过于我们使用Tuple作为initialResult。一旦你尝试将reduce进入到日常工作流中,会渐渐发现,Tuple是一个不错的选择,它能够将数据与reduce操作快速挂钩起来,后面的有趣例子还会用到,相当地契合场景。

2. 源码分析

源码:https://github.com/apple/swift/blob/master/stdlib/public/core/SequenceAlgorithms.swift

reduce第一个函数的源码:


@inlinable

public func reduce<Result>(

_ initialResult: Result,

_ nextPartialResult:

  (_ partialResult: Result, Element) throws -> Result

) rethrows -> Result {

    var accumulator = initialResult

    for element in self {

      accumulator = try nextPartialResult(accumulator, element)

    }

    return accumulator

}

通过以上代码代码,我们可以看出,它做了以下几件事情:

1. 定义 accumulator 临时变量,并赋值 initialResult ;

2. 遍历所有的元素,对于每个元素,调用闭包 nextPartialResult;

3. 将闭包执行的结果赋值给临时变量 accumulator ;

4. 遍历完成后,返回 accumulator 。

reduce第二个函数的源码:


@inlinable

public func reduce<Result>(

into initialResult: __owned Result,

_ updateAccumulatingResult:

    (_ partialResult: inout Result, Element) throws -> ()

  ) rethrows -> Result {

    var accumulator = initialResult

    for element in self {

      try updateAccumulatingResult(&accumulator, element)

    }

    return accumulator

}

通过以上代码代码,我们可以看出,它做了以下几件事情:

1. 定义 accumulator 临时变量,并赋值 initialResult ;

2. 遍历所有的元素,对于每个元素,调用闭包 updateAccumulatingResult,参数是临时变量 accumulator 的地址,闭包执行其实就是更新 accumulator 的值;

3. 遍历完成后,返回 accumulator 。

从以上源码中就能清晰地看出这两个函数的区别:

  • 第一个函数是将闭包执行的结果赋值给临时变量accumulator,然后遍历下一个元素,知道遍历结束,返回accumulator

  • 第二个函数是将临时变量accumulator地址当做闭包的第一个参数,闭包的执行就是在操作accumulator的值。

所以说第一个函数能做到的事情,第二个函数都可以做得到,在有的时候,使用第二个函数,代码还能更简洁。这就看具体的场景,选择使用哪个函数实现。

3. reduce 有趣的拓展

reducemapflatMapcompactMapfilter的一种扩展的形式(后四个函数能干嘛,reduce就能用另外一种方式实现)。reduce的基础思想是将一个序列转换为一个不同类型的数据,期间通过一个累加器accumulator来持续记录递增状态。为了实现这个方法,我们会向reduce方法中传入一个用于处理序列中每个元素的结合闭包nextPartialResult

A. 使用reduce实现map的功能


let arr = [1, 3, 2]

let r1 = arr.reduce([]) { $0 + [$1 * 2] }

// 打印结果:[2, 6, 4]

print(r1)

let r2 = arr.reduce([]) {

    var temp = $0

    temp.append($1 * 2)

    return temp

}

// 打印结果:[2, 6, 4]

print(r2)

let r3 = arr.reduce(into: []) { $0 += [$1 * 2] }

// reduce-into打印结果:[2, 6, 4]

print(r3)

这里提供了两种写法,第一种更为简洁,第二种显得不那么简洁,但是第一种的效率是要比第二种低的,[2, 6] + [4]执行速度要慢于[2, 6].append(4)。倘若在处理庞大的列表,应取代集合 + 集合的方式,转而使用一个可变的 accumulator 变量进行递增。

关于[2, 6] + [4]执行速度要慢于[2, 6].append(4)的效率问题,这里不做拓展,有兴趣的小伙伴可以参考:

https://airspeedvelocity.net/2015/08/03/arrays-linked-lists-and-performance/

B. 使用reduce实现filter的功能


let nums = [1, 2, 3, 4]

let result = nums.reduce([]) { $1 % 2 == 0 ? $0 + [$1] : $0 }

// 打印结果:[2, 4]

print(result)

let r2 = nums.reduce(into: []) { $0 += $1 % 2 == 0 ? [$1] : [] }

// reduce-into打印结果:[2, 4]

print(r2)

C. 使用reduce实现flatMap的功能


let nums = [1, 2, 3, 4]

let reduceNums = nums.reduce([]) { $0 + Array(repeating: $1, count: $1) }

// reduce打印结果:[1, 2, 2, 3, 3, 3, 4, 4, 4, 4]

print(reduceNums)

let r2 = nums.reduce(into: []) { $0 += Array(repeating: $1, count: $1) }

// reduce-into打印结果:[1, 2, 2, 3, 3, 3, 4, 4, 4, 4]

print(r2)

D. 使用reduce实现compactMap的功能


var someAry = ["12", "ad", "33", "cc", "22"]

// var reduceAry: [Int]

var reduceAry = someAry.reduce([Int]()) {

    if let i = Int($1) {

        return $0 + [i]

    }

    return $0

}

// reduce打印结果:[12, 33, 22]

print(reduceAry)

var r2Ary = someAry.reduce(into: [Int]()) {

    if let i = Int($1) {

        $0 += [i]

    }

}

// reduce-into打印结果:[12, 33, 22]

print(r2Ary)

这里只做演示,使用的是最简洁的代码,实际项目中,还是建议使用效率更高的方式。

从这里看来,reduce能做的这些系统已经提供了更好的实现方式,而且性能比reduce来实现要高很多,写这四段代码,只是让我们更深入地理解reduce的实现方式以及它的灵活性,reduce能做的其实更多。

比如: 基于某个标准对一个Int数组做划分,区分奇数和偶数的数组


let nums = [1, 2, 3, 4, 5, 6, 7, 8, 9]

typealias ResTuple = ([Int], [Int])

let res = nums.reduce(([], [])) { (r, i) -> ResTuple in

    if i % 2 == 0 {

        return (r.0, r.1 + [i])

    } else {

        return (r.0 + [i], r.1)

    }

}

// 打印结果:奇数:[1, 3, 5, 7, 9],偶数:[2, 4, 6, 8]

print("奇数:\(res.0),偶数:\(res.1)")

// 留给读者:使用reduce-into应该怎么写呢?

如果使用常规的思路的话,会怎么实现呢?使用reduce是不是代码更加简洁,逻辑更加清晰。

reduce除了较强的灵活性之外,还具有另一个优势:通常情况下,mapfilter所组成的链式结构会引入性能上的问题,因为它们需要多次遍历你的集合才能最终得到结果值,这种操作往往伴随着性能损失,比如以下代码:


[0, 1, 2, 3, 4]

    .map({ $0 + 3})

    .filter({ $0 % 2 == 0})

    .reduce(0, +)

初始序列(即[0, 1, 2, 3, 4])被重复访问了三次之多。首先是map,接着 filter,最后对数组内容求和,对于这种实现方式,实际是浪费了CPU的性能的,如果使用reduce,可以完美替代,且极大提高执行效率:


[0, 1, 2, 3, 4]

    .reduce(0) { ($0 + 3) % 2 == 0 ? $1 + $0 + 3 : $1 }

reduce的实现方式,只需要遍历 1 次就够了,代码也更加简洁。

另外还有一些有趣的例子,或许对于reduce的使用打开思路有所帮助:

A: 返回数组中有多少个不相同的数


let nums = [11, 2, 3, 4, 5, 6, 5, 1]

// let r: (Int?, Int)

let r = nums.sorted(by: <)

        .reduce((.none, 0)) { ($1, $0.0 == $1 ? $0.1 : $0.1 + 1) }

// 打印结果:7

print(r.1)

// 留给读者:使用reduce-into应该怎么写呢?

B: 返回原数组分解成长度为 n 后的多个数组


func chunk<T>(list: [T], length: Int) -> [[T]] {

    typealias Acc = (stack: [[T]], cur: [T], cnt: Int)

    let l = list.reduce((stack: [], cur: [], cnt: 0)) { (ac, o) -> Acc in

        if ac.cnt == length {

            return (stack: ac.stack + [ac.cur], cur: [o], cnt: 1)

        } else {

            return (stack: ac.stack, cur: ac.cur + [o], cnt: ac.cnt + 1)

        }

    }

    return l.stack + [l.cur]

}

// 打印结果:[[1, 2], [3, 4], [5, 6], [7]]

print(chunk(list: [1, 2, 3, 4, 5, 6, 7], length: 2))

// 留给读者:使用reduce-into应该怎么写呢?

C: 给定一个 items 数组,每隔 count 个元素插入 element 元素,返回结果值,且需要确保 element 仅在中间插入,而不会添加到数组尾部


func interpose<T>(items: [T], element: T, count: Int = 1) -> [T] {

    // cur 为当前遍历元素的索引值 cnt 为计数器,当值等于 count 时又重新置 1

    typealias Acc = (ac: [T], cur: Int, cnt: Int)

    return items.reduce((ac: [], cur: 0, cnt: 1), { (a, o) -> Acc in

        switch a {

        // 此时遍历的当前元素为序列中的最后一个元素

        case let (ac, cur, _) where cur + 1 == items.count:

            return (ac + [o], 0, 0)

        // 满足插入条件

        case let (ac, cur, c) where c == count:

            return (ac + [o, element], cur + 1, 1)

        case let (ac, cur, c):

            return (ac + [o], cur + 1, c + 1)

        }

    }).ac

}

// 打印结果:[1, 9, 2, 9, 3, 9, 4, 9, 5]

print(interpose(items: [1, 2, 3, 4, 5], element: 9))

// 打印结果:[1, 2, 9, 3, 4, 9, 5]

print(interpose(items: [1, 2, 3, 4, 5], element: 9, count: 2))

// 留给读者:使用reduce-into应该怎么写呢?

D: 计算来自浙江的考生的高考平均分


let students: [[String: String]] = [["name": "张三", "city": "杭州, ZJ", "score": "580"],

                                  ["name": "李四", "city": "南昌, JX", "score": "520"],

                                  ["name": "王五", "city": "长沙, HN", "score": "536"],

                                  ["name": "赵六", "city": "绍兴, ZJ", "score": "602"],

                                  ["name": "周七", "city": "宁波, ZJ", "score": "599"],

                                  ["name": "吴八", "score": "499"]] // 由于失误,城市丢失了

typealias ResTuple = (cnt: Int, scoreTotal: Int)

let restlt = students.reduce((0, 0)) { (r, s) -> ResTuple in



    // 如果城市缺失,或不是浙江的,或分数缺失则返回

    guard let city = s["city"],

        city.hasSuffix("ZJ"),

        let score = Int(s["score"] ?? "") else {

        return r

    }

    return (r.0 + 1, r.1 + score)

}

// 打印结果:浙江考生的平局分为:593.6667

print("浙江考生的平局分为:\(Float(restlt.1) / Float(restlt.0))")

// 留给读者:使用reduce-into应该怎么写呢?

这些例子,算是抛砖引玉,希望对大家对reduce的理解和使用有所帮助。

--

参考文档:

http://blog.devtang.com/2016/03/05/swift-gym-4-map-and-flatmap/

http://www.hangge.com/blog/cache/detail_1827.html

https://www.jianshu.com/p/56c99d31f2df

https://www.jianshu.com/p/06c90c0470b2

https://blog.csdn.net/offbye/article/details/50856101

https://blog.csdn.net/offbye/article/details/50856101

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

推荐阅读更多精彩内容