Swift 高階函數(shù)


高階函數(shù)的定義:

Wikipedia 中瓦糕,是這么定義高階函數(shù)(higher-order function)的,如果一個(gè)函數(shù):

  • 接收一個(gè)或多個(gè)函數(shù)當(dāng)作參數(shù)

  • 把一個(gè)函數(shù)當(dāng)作返回值

至少滿足以上條件中的一個(gè)的函數(shù),那么這個(gè)函數(shù)就被稱作高階函數(shù)钠四。

使用高階函數(shù)進(jìn)行函數(shù)式編程的優(yōu)勢(shì)

  • 簡(jiǎn)化代碼

  • 使邏輯更加清晰

  • 當(dāng)數(shù)據(jù)比較大的時(shí)候埠偿,高階函數(shù)會(huì)比傳統(tǒng)實(shí)現(xiàn)更快,因?yàn)樗梢圆⑿袌?zhí)行(如運(yùn)行在多核上)

高階函數(shù)在Swift語言中有大量的使用場(chǎng)景户矢,本篇分析 Swift 提供的如下幾個(gè)高階函數(shù):mapflatMap殉疼、compactMap梯浪、filterreduce瓢娜。


一挂洛、map

map方法獲取一個(gè)閉包表達(dá)式作為其唯一參數(shù)。

數(shù)組中的每一個(gè)元素調(diào)用一次該閉包函數(shù)眠砾,并返回該元素所映射的值虏劲。

簡(jiǎn)單說就是數(shù)組中每個(gè)元素通過某種規(guī)則(閉包實(shí)現(xiàn))進(jìn)行轉(zhuǎn)換,最后返回一個(gè)新的數(shù)組。

1. 基本使用

a. 需求:將Int類型數(shù)組中的元素乘以2柒巫,然后轉(zhuǎn)換為String類型的數(shù)組


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

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

// 打印結(jié)果:["2", "4", "6", "8"]

print(strs)

b. 需求:生成一個(gè)新的Int數(shù)組励堡,元素是多少元素就重復(fù)多少個(gè)


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

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

// 打印結(jié)果:[[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]

print(mapNums)

最終返回的是一個(gè)二維數(shù)組。

c. 需求:將String類型的數(shù)組轉(zhuǎn)換為Int類型的數(shù)組


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

// var nilAry: [Int?]

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

// 打印結(jié)果:[Optional(12), nil, Optional(33), nil, Optional(22)]

print(nilAry)

最終返回的是[Int?]堡掏,一個(gè)可選類型的Int數(shù)組应结,且元素中存在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)

}

對(duì)于這個(gè)代碼,我們可以看出游两,它做了以下幾件事情:

1. 構(gòu)造一個(gè)名為 result 且與原數(shù)組的 capacity 一致的新數(shù)組砾层,用于存放新的結(jié)果;

2. 遍歷自己的元素贱案,對(duì)于每個(gè)元素肛炮,調(diào)用閉包的轉(zhuǎn)換函數(shù) transform ,進(jìn)行轉(zhuǎn)換宝踪;

3. 將轉(zhuǎn)換的結(jié)果使用 append 方法放入 result 中侨糟;

4. 遍歷完成后,返回 result 瘩燥。

tips: ContiguousArraySwift提供的更高性能的數(shù)組秕重,幾乎與Array沒什么區(qū)別,如果不涉及Objective-C的混編或需要轉(zhuǎn)NSArray厉膀,完全可以使用ContiguousArray取代Array來使用溶耘,可以有更高的性能。

至于與Array的區(qū)別服鹅,這里就不拓展了凳兵,有興趣的小伙伴Google一下。

二企软、flatMap


// flatMap 定義

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

// Swift 4.1 以前的定義庐扫,4.1之后改名為 compactMap,compactMap時(shí)會(huì)詳細(xì)說明

@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的實(shí)現(xiàn)與map非常類似仗哨,也是數(shù)組中每個(gè)元素通過某種規(guī)則(閉包實(shí)現(xiàn))進(jìn)行轉(zhuǎn)換形庭,最后返回一個(gè)新的數(shù)組。

不過flatMap能把數(shù)組中存有數(shù)組的數(shù)組(二維數(shù)組厌漂、N維數(shù)組)一同打開變成一個(gè)新的數(shù)組萨醒,稱為降維,通俗一點(diǎn)就是把多維數(shù)組都會(huì)拍扁為一維數(shù)組桩卵,通過后面的例子一看就明白了验靡。

flatMap另一個(gè)解包的功能在 4.1 版本之后更名為compactMap,所以在compactMap再做說明雏节。

1. 基本使用

a. 需求:將Int類型數(shù)組中的元素乘以2胜嗓,然后轉(zhuǎn)換為String類型的數(shù)組


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

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

// flatMap打印結(jié)果:["2", "4", "6", "8"]

print(strs)

該例子使用的與map一樣,結(jié)果也是一樣的钩乍,普通情況下辞州,兩者的效果一致。

b. 需求:生成一個(gè)新的Int數(shù)組寥粹,元素是多少元素就重復(fù)多少個(gè)


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

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

// flatMap打印結(jié)果:[1, 2, 2, 3, 3, 3, 4, 4, 4, 4]

print(mapNums)

可以看到变过,flatMap把數(shù)組中的數(shù)組都打開了,最終返回的是一個(gè)一維數(shù)組涝涤。而map返回的是一個(gè)二維數(shù)組媚狰,沒有降維。接下來通過源碼阔拳,來分析兩者的差別崭孤。

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. 構(gòu)造一個(gè)名為 result 的新數(shù)組糊肠,用于存放新的結(jié)果辨宠;

2. 遍歷自己的元素,對(duì)于每個(gè)元素货裹,調(diào)用閉包的轉(zhuǎn)換函數(shù) transform 嗤形,進(jìn)行轉(zhuǎn)換;

3. 將轉(zhuǎn)換的結(jié)果使用 append-contentsOf 方法放入 result 中弧圆;

4. 遍歷完成后赋兵,返回 result 。

仔細(xì)觀察搔预,flatMapmap是有一些區(qū)別的:

A. transform的差別

  • maptransform接收的參數(shù)是數(shù)組元素然后輸出的是閉包執(zhí)行后的類型T毡惜,最終執(zhí)行的結(jié)果的是[T]

  • flatMaptransform接收的參數(shù)是數(shù)組的元素,但輸出的一個(gè)Sequence類型斯撮,最終執(zhí)行的結(jié)果并不是Sequence的數(shù)組经伙,而是Sequence內(nèi)部元素另外組成的數(shù)組,即:[Sequence.Element]

B. 第三個(gè)步驟的差別

  • map使用append方法放入result中勿锅,所以transform之后的結(jié)果是什么類型帕膜,就將什么類型放入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])

// 打印結(jié)果:[1, 3, 2, 4, 5]

print(arrCons)

也就是會(huì)把目標(biāo)Sequence的元素一一取出來酪劫,然后放入定義好的數(shù)組。

所以flatMap必須要求 transform 函數(shù)返回的是一個(gè)Sequence類型寺董,因?yàn)?code>append-contentsOf方法需要的是一個(gè)Sequence類型的參數(shù)覆糟。

Sequence是什么呢 ?Sequence是一個(gè)協(xié)議遮咖,主要有兩個(gè)參數(shù)滩字,一個(gè)是Element,也即是 Sequence里的元素御吞,另一個(gè)則是Iterator(迭代器)麦箍,且自身的Element與迭代器的Element是要相同的where Self.Element == Self.Iterator.Element。迭代器是遵循IteratorProtocol協(xié)議的陶珠,而IteratorProtocol的核心是next()方法挟裂,這個(gè)方法在每次被調(diào)用時(shí)返回序列中的下一個(gè)值,對(duì)Sequence的遍歷揍诽,實(shí)際上調(diào)用的就是迭代器的next()方法话瞧,當(dāng)序列下一個(gè)值為空時(shí),next()返回nil寝姿,也就意味著遍歷結(jié)束交排。Sequence的高階函數(shù)都是在Sequence的擴(kuò)展中定義的。

這里就不拓展了饵筑,有興趣的小伙伴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打印結(jié)果:[[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]

print(mapNums)

// flatMap打印結(jié)果:[1, 2, 2, 3, 3, 3, 4, 4, 4, 4]

print(flatMapNums)

transform執(zhí)行的結(jié)果都是一樣的根资,都是得到一個(gè)數(shù)組架专,差別在于map將數(shù)組直接放入result中,而flatMap將數(shù)組中的元素一一取出來玄帕,更準(zhǔn)確的說是調(diào)用Sequence的迭代器next()方法部脚,將元素一一取出來,然后再放入result中裤纹。

然后說說第一個(gè)例子委刘,用mapflatMap的效果是一樣的,是因?yàn)?code>flatMap閉包執(zhí)行后輸出的Sequence是一個(gè)String類型鹰椒,與用maptransform輸出的類型是一致的锡移,而String是遵循Sequence協(xié)議的并且只有一位Character,所以結(jié)果是一樣的漆际,如果我們轉(zhuǎn)換的是多位的String淆珊,會(huì)是什么效果呢?

看如下的代碼:


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

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

// map打印結(jié)果:["2啥啥啥", "4啥啥啥", "6啥啥啥", "8啥啥啥"]

print(mapStrs)

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

// flatMap打印結(jié)果:["2", "啥", "啥", "啥", "4", "啥", "啥", "啥", "6", "啥", "啥", "啥", "8", "啥", "啥", "啥"]

print(flatMapStrs)

// String 的迭代器測(cè)試奸汇,這也就解釋了flatMap在調(diào)用`append-contentsOf`方法是施符,是將 transform 后得到的 String 的 每一個(gè) 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執(zhí)行的結(jié)果都是一樣的往声,都是得到一個(gè)String,差別在于mapString直接放入result中戳吝,而flatMapString中的Character一一取出來浩销,更準(zhǔn)確的說是調(diào)用String的迭代器next()方法,將Character一一取出來骨坑,然后再放入result中撼嗓。

三柬采、Optional 中的 map 和 flatMap

上面所分析的都是數(shù)組的mapflatMap欢唾,不只是數(shù)組中有這兩個(gè)高階函數(shù),Optional有這兩個(gè)高階函數(shù)粉捻。

先看看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. 需求:修改一個(gè)可選Int的值


let a1: Int? = 1

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

// 打印結(jié)果:Optional(2)

print(b1)

let a2: Int? = nil

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

// 打印結(jié)果:nil

print(b2)

通過例子礁遣,說明對(duì)于一個(gè)Optional的變量來說,map方法允許它再次修改自己的值肩刃,并且不必關(guān)心自己是否為.None祟霍。

b. 需求:對(duì)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)

}

// 打印結(jié)果:Optional("2019-07-16")

print(formatted)

// 使用 map 函數(shù)后,代碼變得更短盈包,更易讀:

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)

通過例子沸呐,我們可以得出結(jié)論,當(dāng)輸入的是一個(gè)Optional呢燥,同時(shí)需要在邏輯中處理這個(gè)Optional是否為nil崭添,那么就適合用map來替代原來的寫法,使得代碼更加簡(jiǎn)短叛氨。

c. 需求:將一個(gè)字符串轉(zhuǎn)換成Int


let s: String? = "abc"

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

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

// Optional(nil) --> map 會(huì)多包一層Optional

print(mapR)

// nil

print(flatMapR)

從上面的例子呼渣,我們可以得出結(jié)論,當(dāng)我們的閉包參數(shù)有可能返回nil的時(shí)候寞埠,使用flatMap會(huì)更加合適屁置,map會(huì)多包一層Optional,這樣就很容易導(dǎo)致多重Optional嵌套的問題仁连。

什么是多重Optional嵌套呢蓝角,Optionalmap使用不當(dāng),就會(huì)導(dǎo)致多重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

    }

}

// 打印結(jié)果:"b is not nil"

if let _ = b {

    print("b is not nil")

} else {

    print("b is nil")

}

由上面的代碼就是Optionalmap使用不當(dāng)而導(dǎo)致的多重Optional嵌套帅容,多重Optionalnilif-let的判定是失效的伍伤,所以在工作中盡量避免多重Optional嵌套問題并徘。上面例子的解決辦法就是將map替換成flatMap,由于flatMap會(huì)有一次解包操作扰魂,所以能避免多重Optional嵌套的問題麦乞。

關(guān)于多重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

    }

  }

這兩函數(shù)是驚人的相似姐直,不仔細(xì)看的話倦淀,甚至看不出這兩個(gè)函數(shù)的差別,兩函數(shù)實(shí)現(xiàn)當(dāng)然是差別的:

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

2. map 把結(jié)果放到 .Some 里面返回撞叽,也就是會(huì)調(diào)用一次 Optional 的構(gòu)造函數(shù),多包一層 Optional 插龄,flatMap把結(jié)果直接返回

兩個(gè)函數(shù)最終都保證了返回結(jié)果是Optional的愿棋,只是將結(jié)果轉(zhuǎn)換成Optional的位置不一樣。

既然OptionalmapflatMap本質(zhì)上是一樣的均牢,為什么要搞兩種形式呢糠雨?這其實(shí)是為了調(diào)用者更方便而設(shè)計(jì)的。調(diào)用者提供的閉包函數(shù)徘跪,既可以返回Optional的結(jié)果甘邀,也可以返回非Optional的結(jié)果。對(duì)于后者垮庐,使用map方法松邪,即可以將結(jié)果繼續(xù)轉(zhuǎn)換成Optional的。結(jié)果是Optional的意味著我們可以繼續(xù)鏈?zhǔn)秸{(diào)用哨查,也更方便我們處理錯(cuò)誤逗抑。

最后我們來看一段代碼:


var arr = [1, 2, 4]

let res = arr.first.flatMap {

    arr.reduce($0, max)

}

// 打印結(jié)果:Optional(4)

print(res)

代碼的意思是:計(jì)算出數(shù)組中的元素最大值,按理說解恰,求最大值直接使用reduce方法就可以了锋八,不過有一種特殊情況需要考慮,即數(shù)組中的元素個(gè)數(shù)為 0 的情況护盈,在這種情況下挟纱,沒有最大值。

所以這里使用OptionalflatMap方法來處理了這種情況腐宋。arrfirst方法返回的結(jié)果是Optional的紊服,當(dāng)數(shù)組為空的時(shí)候,first方法返回.None胸竞,所以欺嗤,這段代碼可以處理數(shù)組元素個(gè)數(shù)為 0 的情況了。


var arr: [Int] = []

let res = arr.first.flatMap {

    arr.reduce($0, max)

}

// nil

print(res)

四卫枝、compactMap

compactMap是在4.1之后對(duì)flatMap的一個(gè)重載方法的重命名煎饼,同樣是數(shù)組中每個(gè)元素通過某種規(guī)則(閉包實(shí)現(xiàn))進(jìn)行轉(zhuǎn)換,最后返回一個(gè)新的數(shù)組校赤,不過compactMap會(huì)將nil剔除吆玖,并對(duì)Optional進(jìn)行解包筒溃。

1. 基本使用

a. 需求:將String類型的數(shù)組轉(zhuǎn)換為Int類型的數(shù)組


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

// var compactMapAry: [Int]

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

// compactMap打印結(jié)果:[12, 33, 22]

print(compactMapAry)

最終返回的是[Int],一個(gè)Int數(shù)組沾乘,并將其中轉(zhuǎn)換失敗的nil過濾掉了怜奖,并對(duì)轉(zhuǎn)換成功的Optional值進(jìn)行了解包。

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. 調(diào)用 _compactMap 方法 傳入 transform;

2. 構(gòu)造一個(gè)名為 result 的新數(shù)組掷匠,用于存放新的結(jié)果滥崩;

3. 遍歷自己的元素,對(duì)于每個(gè)元素槐雾,調(diào)用閉包的轉(zhuǎn)換函數(shù) transform 夭委,進(jìn)行轉(zhuǎn)換幅狮;

4. 將轉(zhuǎn)換的結(jié)果 使用 if - let 后募强,再使用 append 方法放入 result 中;

5. 遍歷完成后崇摄,返回 result 擎值。

從這里就可以得出結(jié)論,compactMapmap的區(qū)別就在于逐抑,maptransform后的結(jié)果直接放入result中鸠儿,而compactMap使用if-let后再放入result中,而if-let的作用就是解包和過濾nil厕氨。

在看如下代碼进每,其中的差別以及為什么會(huì)有差別,就很清晰了命斧。


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

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

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

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

print(mapAry)

// compactMap打印結(jié)果:[12, 33, 22]

print(compactMapAry)

五田晚、filter

filter用來過濾元素,即篩選出數(shù)組元素中滿足某種條件(閉包實(shí)現(xiàn))的元素国葬。

1. 基本使用

a. 需求:篩選出Int數(shù)組中的偶數(shù)


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

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

// 打印結(jié)果:[12, 36, 96]

print(result)

最終返回的是全部是偶數(shù)的Int數(shù)組贤徒。

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. 調(diào)用 _filter 方法 傳入 isIncluded 接奈;

2. 構(gòu)造一個(gè)名為 result 的新數(shù)組,用于存放新的結(jié)果通孽;

3. 使用迭代器序宦,遍歷所有的元素,對(duì)于每個(gè)元素背苦,調(diào)用閉包 isIncluded 互捌,判斷是否符合條件堡僻;

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

5. 遍歷完成后疫剃,返回 result 钉疫。

六、reduce

reduce有兩個(gè)函數(shù)巢价,先看看定義:


@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兩個(gè)函數(shù)都是把數(shù)組元素組合計(jì)算為另一個(gè)值牲阁,并且會(huì)接受一個(gè)初始值,這個(gè)初始值的類型可以和數(shù)組元素類型不同壤躲,這樣子就有很大的可操作空間城菊,也會(huì)很有趣,后面會(huì)說到碉克。

reduce兩個(gè)函數(shù)效果是一樣的凌唬,但也是有差別的,差別就在于閉包的定義:

  • 第一個(gè)函數(shù)閉包漏麦,接收ResultElement客税,返回閉包執(zhí)行后的Result,后續(xù)的操作是將每次閉包執(zhí)行后的Result當(dāng)做下一個(gè)元素執(zhí)行閉包的入?yún)⑺赫辏钡奖闅v完所有元素更耻;

  • 第二個(gè)函數(shù)閉包,接收的依然是ResultElement捏膨,不過沒有返回值秧均,并且Result是用inout修飾的,所以傳入閉包的是Result的地址号涯,所以閉包的執(zhí)行都是基于Result進(jìn)行操作的目胡,這么說可能有些抽象,下面的源碼分析链快,一看就明白了誉己。

還有一個(gè)區(qū)別就是初始值,第二個(gè)函數(shù)使用了__owned進(jìn)行了修飾久又,這個(gè)我也沒懂是為什么巫延,如果有知道的小伙伴望不吝賜教。

這兩個(gè)函數(shù)實(shí)現(xiàn)的效果是一樣的地消,只是實(shí)現(xiàn)的方式不同而已炉峰,第一個(gè)函數(shù)能做到的事情,第二個(gè)函數(shù)都能做到脉执。

1. 基本使用

a. 需求:求一個(gè)Int類型數(shù)組的和


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

// 正經(jīng)寫法

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

// 簡(jiǎn)寫

let r = arr.reduce(0, +)

// 打印結(jié)果:14

print(r)

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

// 打印結(jié)果:14

print(r2)

最終返回的 14疼阔,即 0 + 2 + 3 + 4 + 5 的和,其中0是初始值。除了能獲取和婆廊,當(dāng)然也能能獲取積迅细、商和差。let r = arr.reduce(0, +) 簡(jiǎn)寫中的符號(hào)淘邻,可以使用 + - * /茵典,在/的時(shí)候注意除數(shù)不能為0,可以修改試試看宾舅。

這里能夠簡(jiǎn)寫的原因是由于Swift強(qiáng)大的類型推導(dǎo)统阿,閉包僅僅傳入了一個(gè)+號(hào),Swift推導(dǎo)過程是首先nextPartialResult閉包有兩個(gè)傳入?yún)?shù)ResultElement筹我,除此之外別無其他扶平,因此+只能對(duì)這兩個(gè)參數(shù)求和,得到一個(gè)結(jié)果值稱為x吧蔬蕊,由于nextPartialResult函數(shù)還需要返回一個(gè)結(jié)果值结澄,但是除了x也沒有其他的可能,因此把x作為閉包結(jié)果值返回和Result進(jìn)行相加計(jì)算岸夯,然后返回麻献。

b. 需求:求出數(shù)組中奇數(shù)的和、以及偶數(shù)乘積


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

}

// 打印結(jié)果:奇數(shù)和:4囱修,偶數(shù)乘積:8

print("奇數(shù)和:\(res.0)赎瑰,偶數(shù)乘積:\(res.1)")

let res1 = nums

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

// 打印結(jié)果:reduce-into --> 奇數(shù)和:4王悍,偶數(shù)乘積:8

print("reduce-into --> 奇數(shù)和:\(res1.0)破镰,偶數(shù)乘積:\(res1.1)")

通過這個(gè)例子,reduce能做的不僅僅是求和或拼接压储,還可以做更多個(gè)性化的事情鲜漩。上面實(shí)現(xiàn)中最有意思的莫過于我們使用Tuple作為initialResult。一旦你嘗試將reduce進(jìn)入到日常工作流中集惋,會(huì)漸漸發(fā)現(xiàn)孕似,Tuple是一個(gè)不錯(cuò)的選擇,它能夠?qū)?shù)據(jù)與reduce操作快速掛鉤起來刮刑,后面的有趣例子還會(huì)用到喉祭,相當(dāng)?shù)仄鹾蠄?chǎng)景。

2. 源碼分析

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

reduce第一個(gè)函數(shù)的源碼:


@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 臨時(shí)變量,并賦值 initialResult 翘紊;

2. 遍歷所有的元素蔽氨,對(duì)于每個(gè)元素,調(diào)用閉包 nextPartialResult;

3. 將閉包執(zhí)行的結(jié)果賦值給臨時(shí)變量 accumulator 鹉究;

4. 遍歷完成后宇立,返回 accumulator 。

reduce第二個(gè)函數(shù)的源碼:


@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 臨時(shí)變量资厉,并賦值 initialResult 磕瓷;

2. 遍歷所有的元素,對(duì)于每個(gè)元素慨默,調(diào)用閉包 updateAccumulatingResult,參數(shù)是臨時(shí)變量 accumulator 的地址,閉包執(zhí)行其實(shí)就是更新 accumulator 的值津函;

3. 遍歷完成后,返回 accumulator 孤页。

從以上源碼中就能清晰地看出這兩個(gè)函數(shù)的區(qū)別:

  • 第一個(gè)函數(shù)是將閉包執(zhí)行的結(jié)果賦值給臨時(shí)變量accumulator尔苦,然后遍歷下一個(gè)元素,知道遍歷結(jié)束行施,返回accumulator允坚;

  • 第二個(gè)函數(shù)是將臨時(shí)變量accumulator地址當(dāng)做閉包的第一個(gè)參數(shù),閉包的執(zhí)行就是在操作accumulator的值蛾号。

所以說第一個(gè)函數(shù)能做到的事情稠项,第二個(gè)函數(shù)都可以做得到,在有的時(shí)候鲜结,使用第二個(gè)函數(shù)展运,代碼還能更簡(jiǎn)潔。這就看具體的場(chǎng)景精刷,選擇使用哪個(gè)函數(shù)實(shí)現(xiàn)拗胜。

3. reduce 有趣的拓展

reducemapflatMap怒允、compactMapfilter的一種擴(kuò)展的形式(后四個(gè)函數(shù)能干嘛埂软,reduce就能用另外一種方式實(shí)現(xiàn))。reduce的基礎(chǔ)思想是將一個(gè)序列轉(zhuǎn)換為一個(gè)不同類型的數(shù)據(jù)纫事,期間通過一個(gè)累加器accumulator來持續(xù)記錄遞增狀態(tài)勘畔。為了實(shí)現(xiàn)這個(gè)方法,我們會(huì)向reduce方法中傳入一個(gè)用于處理序列中每個(gè)元素的結(jié)合閉包nextPartialResult丽惶。

A. 使用reduce實(shí)現(xiàn)map的功能


let arr = [1, 3, 2]

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

// 打印結(jié)果:[2, 6, 4]

print(r1)

let r2 = arr.reduce([]) {

    var temp = $0

    temp.append($1 * 2)

    return temp

}

// 打印結(jié)果:[2, 6, 4]

print(r2)

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

// reduce-into打印結(jié)果:[2, 6, 4]

print(r3)

這里提供了兩種寫法炫七,第一種更為簡(jiǎn)潔,第二種顯得不那么簡(jiǎn)潔蚊夫,但是第一種的效率是要比第二種低的诉字,[2, 6] + [4]執(zhí)行速度要慢于[2, 6].append(4)。倘若在處理龐大的列表,應(yīng)取代集合 + 集合的方式壤圃,轉(zhuǎn)而使用一個(gè)可變的 accumulator 變量進(jìn)行遞增陵霉。

關(guān)于[2, 6] + [4]執(zhí)行速度要慢于[2, 6].append(4)的效率問題,這里不做拓展伍绳,有興趣的小伙伴可以參考:

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

B. 使用reduce實(shí)現(xiàn)filter的功能


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

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

// 打印結(jié)果:[2, 4]

print(result)

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

// reduce-into打印結(jié)果:[2, 4]

print(r2)

C. 使用reduce實(shí)現(xiàn)flatMap的功能


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

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

// reduce打印結(jié)果:[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打印結(jié)果:[1, 2, 2, 3, 3, 3, 4, 4, 4, 4]

print(r2)

D. 使用reduce實(shí)現(xiàn)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打印結(jié)果:[12, 33, 22]

print(reduceAry)

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

    if let i = Int($1) {

        $0 += [i]

    }

}

// reduce-into打印結(jié)果:[12, 33, 22]

print(r2Ary)

這里只做演示踊挠,使用的是最簡(jiǎn)潔的代碼,實(shí)際項(xiàng)目中冲杀,還是建議使用效率更高的方式效床。

從這里看來,reduce能做的這些系統(tǒng)已經(jīng)提供了更好的實(shí)現(xiàn)方式权谁,而且性能比reduce來實(shí)現(xiàn)要高很多剩檀,寫這四段代碼,只是讓我們更深入地理解reduce的實(shí)現(xiàn)方式以及它的靈活性旺芽,reduce能做的其實(shí)更多沪猴。

比如: 基于某個(gè)標(biāo)準(zhǔn)對(duì)一個(gè)Int數(shù)組做劃分,區(qū)分奇數(shù)和偶數(shù)的數(shù)組


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)

    }

}

// 打印結(jié)果:奇數(shù):[1, 3, 5, 7, 9]采章,偶數(shù):[2, 4, 6, 8]

print("奇數(shù):\(res.0)运嗜,偶數(shù):\(res.1)")

// 留給讀者:使用reduce-into應(yīng)該怎么寫呢?

如果使用常規(guī)的思路的話悯舟,會(huì)怎么實(shí)現(xiàn)呢担租?使用reduce是不是代碼更加簡(jiǎn)潔,邏輯更加清晰抵怎。

reduce除了較強(qiáng)的靈活性之外奋救,還具有另一個(gè)優(yōu)勢(shì):通常情況下,mapfilter所組成的鏈?zhǔn)浇Y(jié)構(gòu)會(huì)引入性能上的問題便贵,因?yàn)樗鼈冃枰啻伪闅v你的集合才能最終得到結(jié)果值菠镇,這種操作往往伴隨著性能損失,比如以下代碼:


[0, 1, 2, 3, 4]

    .map({ $0 + 3})

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

    .reduce(0, +)

初始序列(即[0, 1, 2, 3, 4])被重復(fù)訪問了三次之多承璃。首先是map,接著 filter蚌本,最后對(duì)數(shù)組內(nèi)容求和盔粹,對(duì)于這種實(shí)現(xiàn)方式,實(shí)際是浪費(fèi)了CPU的性能的程癌,如果使用reduce舷嗡,可以完美替代,且極大提高執(zhí)行效率:


[0, 1, 2, 3, 4]

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

reduce的實(shí)現(xiàn)方式嵌莉,只需要遍歷 1 次就夠了进萄,代碼也更加簡(jiǎn)潔。

另外還有一些有趣的例子,或許對(duì)于reduce的使用打開思路有所幫助:

A: 返回?cái)?shù)組中有多少個(gè)不相同的數(shù)


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) }

// 打印結(jié)果:7

print(r.1)

// 留給讀者:使用reduce-into應(yīng)該怎么寫呢中鼠?

B: 返回原數(shù)組分解成長(zhǎng)度為 n 后的多個(gè)數(shù)組


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]

}

// 打印結(jié)果:[[1, 2], [3, 4], [5, 6], [7]]

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

// 留給讀者:使用reduce-into應(yīng)該怎么寫呢可婶?

C: 給定一個(gè) items 數(shù)組,每隔 count 個(gè)元素插入 element 元素援雇,返回結(jié)果值矛渴,且需要確保 element 僅在中間插入,而不會(huì)添加到數(shù)組尾部


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

    // cur 為當(dāng)前遍歷元素的索引值 cnt 為計(jì)數(shù)器惫搏,當(dāng)值等于 count 時(shí)又重新置 1

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

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

        switch a {

        // 此時(shí)遍歷的當(dāng)前元素為序列中的最后一個(gè)元素

        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

}

// 打印結(jié)果:[1, 9, 2, 9, 3, 9, 4, 9, 5]

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

// 打印結(jié)果:[1, 2, 9, 3, 4, 9, 5]

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

// 留給讀者:使用reduce-into應(yīng)該怎么寫呢具温?

D: 計(jì)算來自浙江的考生的高考平均分


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

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

                                  ["name": "王五", "city": "長(zhǎng)沙, 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



    // 如果城市缺失筐赔,或不是浙江的铣猩,或分?jǐn)?shù)缺失則返回

    guard let city = s["city"],

        city.hasSuffix("ZJ"),

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

        return r

    }

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

}

// 打印結(jié)果:浙江考生的平局分為:593.6667

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

// 留給讀者:使用reduce-into應(yīng)該怎么寫呢?

這些例子茴丰,算是拋磚引玉剂习,希望對(duì)大家對(duì)reduce的理解和使用有所幫助。

--

參考文檔:

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

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

http://www.reibang.com/p/56c99d31f2df

http://www.reibang.com/p/06c90c0470b2

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末较沪,一起剝皮案震驚了整個(gè)濱河市鳞绕,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌尸曼,老刑警劉巖们何,帶你破解...
    沈念sama閱讀 206,482評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異控轿,居然都是意外死亡冤竹,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,377評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門茬射,熙熙樓的掌柜王于貴愁眉苦臉地迎上來鹦蠕,“玉大人,你說我怎么就攤上這事在抛≈硬。” “怎么了?”我有些...
    開封第一講書人閱讀 152,762評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵刚梭,是天一觀的道長(zhǎng)肠阱。 經(jīng)常有香客問我,道長(zhǎng)朴读,這世上最難降的妖魔是什么屹徘? 我笑而不...
    開封第一講書人閱讀 55,273評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮衅金,結(jié)果婚禮上噪伊,老公的妹妹穿的比我還像新娘簿煌。我一直安慰自己,他們只是感情好鉴吹,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,289評(píng)論 5 373
  • 文/花漫 我一把揭開白布姨伟。 她就那樣靜靜地躺著,像睡著了一般拙寡。 火紅的嫁衣襯著肌膚如雪授滓。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,046評(píng)論 1 285
  • 那天肆糕,我揣著相機(jī)與錄音般堆,去河邊找鬼。 笑死诚啃,一個(gè)胖子當(dāng)著我的面吹牛淮摔,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播始赎,決...
    沈念sama閱讀 38,351評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼和橙,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了造垛?” 一聲冷哼從身側(cè)響起魔招,我...
    開封第一講書人閱讀 36,988評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎五辽,沒想到半個(gè)月后办斑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,476評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡杆逗,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,948評(píng)論 2 324
  • 正文 我和宋清朗相戀三年乡翅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片罪郊。...
    茶點(diǎn)故事閱讀 38,064評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡蠕蚜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出悔橄,到底是詐尸還是另有隱情靶累,我是刑警寧澤,帶...
    沈念sama閱讀 33,712評(píng)論 4 323
  • 正文 年R本政府宣布橄维,位于F島的核電站尺铣,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏争舞。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,261評(píng)論 3 307
  • 文/蒙蒙 一澈灼、第九天 我趴在偏房一處隱蔽的房頂上張望竞川。 院中可真熱鬧店溢,春花似錦、人聲如沸委乌。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,264評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽遭贸。三九已至戈咳,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間壕吹,已是汗流浹背著蛙。 一陣腳步聲響...
    開封第一講書人閱讀 31,486評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留耳贬,地道東北人踏堡。 一個(gè)月前我還...
    沈念sama閱讀 45,511評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像咒劲,于是被迫代替她去往敵國和親顷蟆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,802評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容