Swift中的高階函數: Filter, Map, Reduce, flatmap, compactMap(譯)

自從我詳細瞭解函數和閉包後,我就想去知道它們的優勢和在編程中的使用, 個人理解是高階函數的使用是基於集合類型的。編程

根據個人理解,高階函數就是把另外一個函數或者閉包當作參數而且返回值。swift

首先,我來解釋一下。思考下面的代碼將會使你對高階函數的理解更深:api

// 將函數當作參數傳遞給另外一個函數

func addation(num1: Double, num2: Double) -> Double {
    return num1 + num2
}

func multiply(num1: Double, num2: Double) -> Double {
    return num1 * num2
}

func doMathOperation(operation: (_ x: Double, _ y: Double) -> Double, num1: Double, num2: Double) -> Double {
    return operation(num1, num2)
}

// print 12
doMathOperation(operation: addation(num1:num2:), num1: 10, num2: 2)
//print 20
doMathOperation(operation: multiply(num1:num2:), num1: 10, num2: 2)
複製代碼

addation(num1:num2:)multiply(num1:num2:)都是(Double,Double)->Double的函數。addation(num1:num2:)接受兩個 Double 的值,並返回它們的和;multiply(num1:num2:)接受兩個 Double 的值,並返回它們的乘積。doMathOperation(operation:num1:num2:)則是一個接受三個參數的高階函數,它接受兩個 Double 的參數和一個(Double,Double)->Double 類型的函數。看一下該函數的調用,你應該就理解了高階函數的工做原理。數組

在Swift中,函數和閉包是一等公民。它們能夠存儲在變量中被傳遞。bash

// 函數返回值爲另外一個函數
func doArithmeticOperation(isMultiply: Bool) -> (Double, Double) -> Double {
    func addation(num1: Double, num2: Double) -> Double {
        return num1 + num2
    }
    
    func multiply(num1: Double, num2: Double) -> Double {
        return num1 * num2
    }
    
    return isMultiply ? multiply : addation
}

let operationToPerform1 = doArithmeticOperation(isMultiply: true)
let operationToPerform2 = doArithmeticOperation(isMultiply: false)

operationToPerform1(10, 2) //20
operationToPerform2(10, 2) //12
複製代碼

在上面的代碼中,doArithmeticOperation(isMultiply:)是一個返回值類型爲(Double,Double)->Double的高階函數。它基於一個布爾值來判斷返回值。operationToPerform1 執行乘的操做,operationToPerform2 執行加的操做。經過上面的函數定義和調用,你就理解全部的事情了。閉包

固然,你能夠經過多種不一樣的方法來實現同樣的東西。你可使用閉包來代替函數,你可使用枚舉來判斷函數的操做。在這裏,我只是經過上面的代碼來解釋一下什麼是高階函數。app

下面是 Swift 庫中自帶的一些高階函數,若是我理解沒錯的話,下面的函數接受閉包類型的參數。你能夠在集合類型(Array Set Dictionary)中使用它們。若是你對閉包不太瞭解,你能夠經過這篇文章來學習。函數

Map

map 函數的做用就是對集合進行一個循環,循環內部再對每一個元素作同一個操做。它返回一個包含映射後元素的數組。學習

Map on Array:

假設咱們有一個整數型數組:ui

let arrayOfInt = [2,3,4,5,4,7,2]
複製代碼

咱們如何讓每一個數都乘10呢?咱們一般使用 for-in 來遍歷每一個元素,而後執行相關操做。

var newArr: [Int] = []
for value in arrayOfInt { newArr.append(value*10) }
print(newArr) // prints [20, 30, 40, 50, 40, 70, 20]
複製代碼

上面的代碼看着有點冗餘。它包含建立一個新數組的樣板代碼,咱們能夠經過使用 map 來避免。咱們經過 Swift 的自動補全功能能夠看到 map 函數接受有一個 Int 類型的參數並返回一個泛型的閉包。

let newArrUsingMap = arrayOfInt.map { $0 * 10 }
複製代碼

對於一個整型數組,這是 map 的極簡版本。咱們能夠在閉包中使用 $ 操做符來代指遍歷的每一個元素。

下面的代碼做用都是同樣的,它們表示了一個從繁到簡的過程。經過下面的代碼,你應該對閉包有了一個清晰的認識。

map 的工做原理:map 函數接受一個閉包做爲參數,在迭代集合時調用該閉包。這個閉包映射集合中的元素,並將結果返回。map 函數再將結果放在數組中返回。

Map on Dictionary:

假設咱們有一個書名當作 key ,書的價格當作 value 的字典。

let bookAmount = [「harrypotter」:100.0, 「junglebook」:100.0]
複製代碼

若是你試圖 map 這個字典,Swift 的自動補全將是這樣:

let bookAmount = ["harrypotter": 100.0, "junglebook": 100.0]
let returnFormatMap = bookAmount.map { (key, value) 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)
//[20.340960000000003, 13.1232, 29.199120000000004]
複製代碼

在上面的代碼中,咱們有一個值類型爲 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 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) //[2, 4, 6, 8]
複製代碼

就像 map ,這是一個簡單的函數去篩選集合中的元素。

若是咱們對整形數組使用 filter ,Swift 的自動補全展現以下:

如你所見, filter 函數調用了一個接受一個 Int 類型參數、返回值爲 Bool 類型、名字爲 isIncluded 的閉包。isIncluded 在每次遍歷都會返回一個布爾值,而後基於布爾值將建立一個新的包含篩選結果的數組。

咱們能夠經過 filter 將上面的代碼修改成:

var newArray = arrayOfIntegers.filter { (value) -> Bool in
    return value % 2 == 0
}
複製代碼

filter 閉包也能夠被簡化,就像 map:

Filter on dictionary

假設有一個書名當作 key ,書的價格當作 value 的字典。

let bookAmount = ["harrypotter":100.0, "junglebook":1000.0]
複製代碼

若是你想對這個字典調用 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 :聯合集合中全部的值,並返回一個新值。

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 reducedNumberSum = numbers.reduce(0) { $0 + $1 }
print(reducedNumberSum) // prints 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

let reduce2 = bookAmount.reduce("book are ") { (result, tuple) in
    return result + tuple.key + " "
}
print(reduce2) //book are junglebook harrypotter 
複製代碼

對於字典,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 操做。 Apple docs 解釋: 返回一個對序列的每一個元素進行形變的串級結果( Returns an array containing the concatenated results of calling the given transformation with each element of this sequence.)

解讀 : map + (Flat the collection)
複製代碼

圖1 對 flatmap 進行代碼說明

圖2

在圖2 中,flatMap 迭代 collections 中的全部 collection 進行大寫操做。在這個例子中,每一個 collection 是字符串。下面是執行步驟:

  1. 對全部的字符串執行 upperCased() 函數,這相似於:
[「abc」,」def」,」ghi」].map { $0.uppercased() }
複製代碼

輸出:

output: [「ABC」, 「DEF」, 「GHI」]
複製代碼
  1. 將 collections 鋪平爲一個 collection。
output: ["A", "B", "C", "D", "E", "F", "G", "H", "I"]
複製代碼

注意:在 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)
//["key4": 4, "key2": 1, "key1": 0, "key3": 3]
複製代碼

Flatmap on set

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 onlyEven = collections.flatMap { $0.filter { $0 % 2 == 0 } }
複製代碼

鏈式 : (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)
複製代碼

Tip

let arr = [1, nil, 3, 4, nil]
let result = arr.map { $0 }
print(result) //[Optional(1), nil, Optional(3), Optional(4), nil]
複製代碼

使用 map 是這樣的。

總結

好了,到這裏這篇文章也就接近尾聲了。

咱們要儘量的使用高階函數:

  • 它能夠提升你的 Swift 技能
  • 代碼可讀性更高
  • 更加函數式
相關文章
相關標籤/搜索