swift小知識點之高階函數之map, filter, reduce


初探高階函數

在 Swift 中,高階函數一共有下面幾個:

  • map:對給定數組每個元素,執行閉包中的映射,將映射結果放置在數組中返回。

  • flatMap:對給定數組的每個元素,執行閉包中的映射,對映射結果進行合並操作然后將合並操作后的結果放置在數組中返回。

  • compactMap:對給定數組的每個元素,執行閉包中的映射,將非空的映射結果放置在數組中返回或者將非空的映射結果-鍵值對放置在字典中返回。

  • filter:對給定數組的每個元素,執行閉包中的操作,將符合條件的元素放在數組中返回。

  • reduce:對給定數組的每個元素,執行閉包中的操作對元素進行合並,並將合並結果返回。

Map

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

簡單說就是數組中每個元素通過某個方法進行轉換,最后返回一個新的數組。

  • Map on Array:

    假設我們有一個整數型數組,我們如何讓每個數都乘10呢?我們通常使用 for-in 來遍歷每個元素,然后執行相關操作。
    let arrayOfInt = [2,3,4,5,4,7,2]
    var newArr:[Int] = []
    for value in arrayOfInt {
        newArr.append(value * 10)
    }
    print(newArr)
    /*打印: [20, 30, 40, 50, 40, 70, 20] */

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

    let arrayOfInt = [2,3,4,5,4,7,2]
    let newArr = arrayOfInt.map { $0 * 10 }
    print(newArr)
    /*打印: [20, 30, 40, 50, 40, 70, 20] */

    對於一個整型數組,這是 map 的極簡版本。我們可以在閉包中使用 $ 操作符來代指遍歷的每個元素。
    下面的代碼作用都是一樣的,它們表示了一個從繁到簡的過程。通過下面的代碼,你應該對閉包有了一個清晰的認識。

    arrayOfInt.map ({ (someInt:Int) -> Int in return someInt * 10}) //1.閉包語法
    arrayOfInt.map ({ (someInt:Int) in return someInt * 10}) //2.省略返回值
    arrayOfInt.map ({ someInt in return someInt * 10}) //3.省略指定參數類型
    arrayOfInt.map ({ someInt in someInt * 10}) //4.省略return
    arrayOfInt.map { $0 * 10} //5.尾隨閉包語法
    /*打印: [20, 30, 40, 50, 40, 70, 20] */

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

  • Map on Dictionary:

    假設我們有一個書名當做 key ,書的價格當做 value 的字典。如果你試圖 map 這個字典,Swift 的自動補全將是這樣:
    let bookAmount = ["harrypotter":100.0, "junglebook":100.0]
    let returnFormatMap = bookAmount.map { (key: String, value: Double) 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)
    /*打印: [29.199120000000004, 13.1232, 20.340960000000003] */

    在上面的代碼中,我們有一個值類型為 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函數的作用是遍歷該集合,然后將該集合中符合某些特定條件的元素組成新的數組,並返回該新數組,filter函數就是選擇集合中符合條件的元素,過濾掉不符合條件的元素。

filter函數只有一個閉包作為參數,該閉包指定了篩選條件。該閉包使用集合對象中的一個元素作為參數,並且必須返回一個Bool值以表明該元素是否應該被包含新的數組中並返回。

  • 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)
    /*打印:["0:1", "1:2", "2:4", "3:5"]*/

    就像 map ,這是一個簡單的函數去篩選集合中的元素。
    如果我們對整形數組使用 filter ,Swift 的自動補全展示如下:

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

    我們可以通過 filter 將上面的代碼修改為:

    let arrayOfIntegers = [1,2,3,4,5,6,7,8,9]
    var newArray = arrayOfIntegers.filter {(value) -> Bool in return value % 2 == 0}
    print(newArray)
    /*打印:[2, 4, 6, 8]*/

    filter 閉包也可以被簡化,就像 map:

    let arrayOfIntegers = [1,2,3,4,5,6,7,8,9]
    arrayOfIntegers.filter({(someInt:Int) -> Bool in return someInt % 2 == 0}) ///1.閉包語法
    arrayOfIntegers.filter({(someInt:Int) in return someInt % 2 == 0}) ///2.省略返回值
    arrayOfIntegers.filter({someInt in return someInt % 2 == 0}) ///3.省略參數指定類型
    arrayOfIntegers.filter({$0 % 2 == 0}) ///4.省略return關鍵字
    arrayOfIntegers.filter {$0 % 2 == 0} ///5.尾隨閉包語法
  • Filter on dictionary

    假設有一個書名當做 key ,書的價格當做 value 的字典。如果你想對這個字典調用 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 :聯合集合中所有的值,並返回一個新值。即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 numbers = [1,2,3,4]
    let reducedNumberSum = numbers.reduce(0) { $0 + $1 }
    print(reducedNumberSum) // prints 10
    /*打印: 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
    /*打印: 1110.0 */
    
    let reduce2 = bookAmount.reduce("book are ") { (result, tuple) in
        return result + tuple.key + " "
    }
    print(reduce2) //book are junglebook harrypotter
    /*打印: book are harrypotter junglebook  */

    對於字典,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 操作。顧名思義FlatMap是將多個集合糅合成一個集合

  • Apple docs解釋:返回一個對序列的每個元素進行形變的串級結果( Returns an array containing the concatenated results of calling the given transformation with each element of this sequence.)
  • 解讀 : map + (Flat the collection)
let codes = [["abc","def","ghi"],["abc","def","ghi"]]
let newCodes = codes.flatMap{ $0.map{$0.uppercased()}}
print(newCodes)
/*打印:["ABC", "DEF", "GHI", "ABC", "DEF", "GHI"]*/
let codes = ["abc","def","ghi"]
let newCodes = codes.flatMap{$0.uppercased()}
print(newCodes)
/*打印:["A", "B", "C", "D", "E", "F", "G", "H", "I"]*/ 

在上面代碼中,flatMap 迭代 collections 中的所有 collection 進行大寫操作。在這個例子中,每個 collection 是字符串。下面是執行步驟:

  1. 對所有的字符串執行 upperCased() 函數,這類似於: 
    [“abc”,”def”,”ghi”].map { $0.uppercased() }
    輸出:
    [“ABC”, “DEF”, “GHI”]
  1. 將 collections 鋪平為一個 collection。
    output: ["A", "B", "C", "D", "E", "F", "G", "H", "I"]
let codes = "abhi muralidharan"
let newCodes = codes.flatMap{return $0}
print(newCodes)
/*打印:
 ["a", "b", "h", "i", " ", "m", "u", "r", "a", "l", "i", "d", "h", "a", "r", "a", "n"]
 */

注意:在 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)
    /*打印:
     ["key2": 1, "key3": 3, "key4": 4, "key1": 0]
     */
  • Flatmap on set

    let numberSet:Set = [Set([4.0,6.2,8.9]),Set([9.9])]
    let flatmapSet = numberSet.flatMap{$0}
    print(flatmapSet)
    /*打印:
     [9.9, 4.0, 8.9, 6.2]
     */
  • 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 collections = [[5, 2, 7], [4, 8], [9, 1, 3]]
    let onlyEven = collections.flatMap { $0.filter { $0 % 2 == 0 } }
    print(onlyEven)
    /*打印:
     [2, 4, 8]
     */

鏈式 : (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)
/*打印: ["key2": 20]*/

使用 map 是這樣的。

let arr = [1, nil, 3, 4, nil]
let result = arr.map { $0 }
/*打印:[Optional(1), nil, Optional(3), Optional(4), nil] */

總結

以后在使用swift編碼的過程中,當你意識到自己在對一個集合對象進行遍歷操作時,你該思考一下是否可以使用map, filter或者reduce函數來替代。

以下是總結了以下map, filter, reduce的特性,

  • 使用函數式編程不僅能減少代碼的行數,還可使用鏈式結構構建復雜的邏輯。
  • map返回了一個結果集,該集合中包含的所有元素是來於對源數組中每一個元素進行相同的轉換之后形成的新元素。
    當你需要映射一個數組,並且不需要改變返回數組的層級結構的時候,使用  map ,反之,則使用 flatMap 。

    當返回數組中的值必須非空的時候,使用 compactMap ;當返回字典中的鍵值對中的value 必須為非空的時候,使用 compactMapValues 。

  • filter返回一個結果集,該集合包含的元素是源數組中的每一個符合篩選條件的元素。

    當你需要查詢的時候,使用 filter 。

  • reduce返回一個值,該值是對初始值和集合中的每個元素調用閉包中相同的操作生成的。

    當你需要將數組進行某種計算並返回一個值得時候,使用 reduce 。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM