Swift高階函數解析 一、鍥子 二、高階函數解析 三、以reduce實現其他高階函數 四、reduce的妙用 五、轉變思想

一、鍥子

最近在學習Swift過程中發現,Swift有不少高階函數,這些函數爲Swift支持函數式編程範式提供了強有力的支持。這些函數主要包括:map、filter、flatMap、compactMap、reduce等。
本文對這些函數進行解析,與網絡上的其他文章相比,本文有兩大特色:

  • 針對這些函數原型進行說明(如果不瞭解泛型,看到這些函數原型時,可能一臉懵逼;理解了這些原型,就更加理解函數本身)
  • reduce方法是很強大的,本文使用reduce方法重新實現其他的高階函數

注意
本文的函數介紹以Array的爲例,其他的十分類似。

二、高階函數解析

2.1 map

  • 作用
    如果用一句話來概括map的作用,那麼就是:以序列的每個元素爲實參來調用用戶提供的轉換方法來獲取一個新的結果,並用這個結果構成一個新的序列
  • 函數原型
@inlinable public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]

// @inlinable 這是在SE-0193中說明的屬性,用於告知編譯器可以爲泛型的具體類型替換生成內聯代碼,至於是否最終內聯,是由編譯器決定的
// public 可以被其他模塊看到該方法
// func 聲明一個函數的關鍵字
// map 函數名
// <T> 這是泛型函數聲明泛型類型佔位符的方式,在顯式、隱式特化時,會被替換爲具體的類型
// _ 標識函數參數的外部參數名被忽略
// transform 是一個參數名
// (Element) throws -> T 這裏標識transform的類型,該類型是一個函數,該函數有一個形參(形參的類型是序列的類型,Element),該函數可能拋出錯誤(throws),該函數的返回值是T
// rethrows 標識,如果用戶提供的錯誤拋出異常,則會再次拋出給用戶
// [T]標識最後返回的是一個數組,數組的類型是T
  • 示例
// 這是一個很普通的例子,僅用於說明map的最基礎功能
let arrInt = [0, 1, 2, 3, 4]
let arrNew = arrInt.map { (aElement) -> String in
    String(aElement)
}
print(arrNew) // ["0", "1", "2", "3", "4"]
// 此示例用於說明在可能碰到nil值時的處理方式
let arrString = ["0", "1", "2", "3", "4", "wwj"]
let arrNew = arrString.map { (aElement) -> Int? in
    Int(aElement)
}
print(arrNew) //[Optional(0), Optional(1), Optional(2), Optional(3), Optional(4), nil]

// 由於"wwj"無法轉換爲數字,所以筆者提供的轉換方法返回了一個Int?
// 這導致最終的arrNew的元素類型是Int?
// 這在某些情況下不方便,所以就請我們看看compactMap是如何處理可選值的

2.2 compactMap

  • 作用
    compactMap的作用於map類似,差別是針對nil值的處理方式不同,compactMap把nil從最終結果過濾出去。
  • 函數原型
@inlinable public func compactMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]

// 此函數原型沒有新的內容,所以不再做介紹
  • 示例
let arrString = ["0", "1", "2", "3", "4", "wwj"]
let arrNew = arrString.compactMap { (aElement) -> Int? in
    Int(aElement)
}
print(arrNew) // [0, 1, 2, 3, 4]

// 可以看出,最終結果arrNew元素的類型是Int,且不包含nil

// 以上方法可以替換爲下面的方法
// 差別在筆者提供的類型轉換方法當中
// 如果對這裏不懂或者想了解更多內容請參閱筆者的《Chapter 2.Functions》
let arrString = ["0", "1", "2", "3", "4", "wwj"]
let arrNew = arrString.compactMap {Int($0)}
print(arrNew) // [0, 1, 2, 3, 4]

2.3 flatMap

  • 作用
    flatMap的作用於map類似,差別是此處用戶提供的轉換方法返回值是一個數組、最終返回的數組是把用戶返回的數組拉平了。
  • 函數原型
@inlinable public func flatMap<SegmentOfResult>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Element] where SegmentOfResult : Sequence

// 這裏只針對與map原型相比,不同的地方進行介紹

// [SegmentOfResult.Element] 與map相比,此處返回的是數組
// where 在此處是對泛型的類型進行約束
// SegmentOfResult : Sequence 返回值的類型佔位符是SegmentOfResult,此處要求返回數組的元素類型必須符合Sequence協議
  • 示例
let arrString = ["0", "12", "32", "34", "54", "wwj"]
let arrNew = arrString.flatMap { (aElement) -> [Character] in
    aElement.shuffled()
}
print(arrNew) // ["0", "2", "1", "3", "2", "3", "4", "4", "5", "w", "w", "j"]

// 字符串其實也是符合序列協議的類型
// 字符串這種序列的元素類型是Character
// shuffled 方法是把組成字符串的字符打亂順序後以數組的形式返回所有的字符

2.4 filter

  • 作用
    flatMap的作用於map類似,差別是用戶提供的函數作爲一個過濾網,通過網的元素組成新的數組,並返回該數組。
  • 函數原型
@inlinable public func filter(_ isIncluded: (String) throws -> Bool) rethrows -> [String]
// 此原型沒有新內容,故不作解釋
  • 示例
let arrString = ["0", "12", "32", "34", "54", "wwj"]
let arrNew = arrString.filter { (aElement) -> Bool in
    aElement.contains("2")
}

print(arrNew) // ["12", "32"]

2.5 reduce

reduce是一個重載函數,其具有兩種類型,下面分別做介紹。

2.5.1 第一種reduce

  • 作用
    使用用戶提供的函數對序列的元素進行結合。如果是第一次看到的話,不好理解,一會看代碼就理解了。
  • 函數原型
@inlinable public func reduce<Result>(_ initialResult: Result, _ nextPartialResult: (Result, Element) throws -> Result) rethrows -> Result
// 此原型沒有新內容,故不作解釋
  • 示例
let arrString = ["0", "12", "32", "34", "54", "wwj"]
let arrNew = arrString.reduce("") { (acc, aElement) -> String in
    return acc + aElement
}
print(arrNew) // 012323454wwj

// 這個例子簡單說明了reduce的用法,很多介紹的文章也只說到這裏
// 此函數的計算過程是這樣子的
// 使用初始值,作爲實參調用用戶的函數,此時得到一個返回值
// 然後以新的返回值作爲實參,再次調用用戶的函數
// 重複上面步驟,指導序列結束
// 把用戶函數最後一次調用的返回值,作爲reduce的返回值,返回給用戶

2.5.2 第二種reduce

  • 作用
    使用用戶提供的函數對序列的元素進行結合。如果是第一次看到的話,不好理解,一會看代碼就理解了。
  • 函數原型
@inlinable public func reduce<Result>(into initialResult: Result, _ updateAccumulatingResult: (inout Result, Element) throws -> ()) rethrows -> Result
// inout 這裏的Result的類型是一個可改變的類型,這很重要
  • 示例
let arrString = ["0", "12", "32", "34", "54", "wwj"]
let strNew = arrString.reduce(into: "") { (acc, aElement) in
    acc.append(aElement) // (1)請注意這裏
}
print(strNew) // 012323454wwj

// 這個方法的返回值與上面一個相同,但是請注意(1)這一行,他是通過修改acc來實現的,也就是說acc是一個輸入輸出參數

三、以reduce實現其他高階函數

大部分網文在講述的時候,都只涉及以上內容,但不包括第二種reduce函數。其實,其作用非常強大,下面我們就用其來實現其他幾個高階函數。

3.1 reduce實現map

let arrInt = [0, 1, 2, 3, 4]
let arrNew = arrInt.reduce(into: [String]()) { (acc, aElement) in
    acc.append(String(aElement))
}
print(arrNew) // ["0", "1", "2", "3", "4"]

3.2 reduce實現compactMap

let arrString = ["0", "1", "2", "3", "4", "wwj"]
let arrNew = arrString.reduce(into: [Int]()) { (acc, aElement) in
    if let aStr = Int(aElement) {
        acc.append(aStr)
    }
}
print(arrNew) // [0, 1, 2, 3, 4]

3.3 reduce實現flatMap

let arrString = ["0", "12", "32", "34", "54", "wwj"]
let arrNew = arrString.reduce(into: [Character](), { (acc, aElement) in
    acc.append(contentsOf: aElement.shuffled())
})
    
print(arrNew) // ["0", "2", "1", "3", "2", "3", "4", "4", "5", "w", "w", "j"]

3.3 reduce實現filter

let arrString = ["0", "12", "32", "34", "54", "wwj"]
let arrNew = arrString.reduce(into: [String]()) { (acc, aElement) in
    if aElement.contains("2") {
        acc.append(aElement)
    }
}

print(arrNew) // ["12", "32"]

四、reduce的妙用

通過第三節,可以看出來,reduce十分強大,可以實現其他高階函數。其實,reduce的強大超過你的想象。一些看起來不可能用reduce實現的功能,都可以用reduce實現。下面,我們看一些示例。

4.1 根據考分進行分級

struct Student {
    let name: String
    let scroeTotale: Int
}

let allStudent = [
    Student(name: "wwj", scroeTotale: 12),
    Student(name: "wzy", scroeTotale: 21),
    Student(name: "sll", scroeTotale: 19),
    Student(name: "sr", scroeTotale: 1),
    Student(name: "sxx", scroeTotale: -1)
]


let studentGrade = allStudent.reduce(into: [String: [Student]]()) { ( acc, aStudent) in

    let grade: String
    switch aStudent.scroeTotale {
    case ...0:
        grade = "差等生"
    case 1...10:
        grade = "低等生"
    case 11...20:
        grade = "中等生"
    case 21...:
        grade = "優等生"
    default:
        grade = "編譯器導致此處必須包含default子句,不然會報錯誤:switch是不完整的"
    }

    if let _ = acc[grade] {
        acc[grade]?.append(aStudent)
    } else {
        acc[grade] = [aStudent]
    }
}
/*
["低等生": [myLearnSwift.Student(name: "sr", scroeTotale: 1)], 
"優等生": [myLearnSwift.Student(name: "wzy", scroeTotale: 21)],
"差等生": [myLearnSwift.Student(name: "sxx", scroeTotale: -1)],
"中等生": [myLearnSwift.Student(name: "wwj", scroeTotale: 12), myLearnSwift.Student(name: "sll", scroeTotale: 19)]]
*/

4.2 獲得優等生的人數和平均分

struct Student {
    let name: String
    let scroeTotale: Int
}

let allStudent = [
    Student(name: "wwj", scroeTotale: 12),
    Student(name: "wzy", scroeTotale: 21),
    Student(name: "sll", scroeTotale: 19),
    Student(name: "sr", scroeTotale: 1),
    Student(name: "sxx", scroeTotale: -1)
]

let studentGrade = allStudent.reduce(into: (number: 0, average: 0)) { ( acc, aStudent) in

    if aStudent.scroeTotale > 10 {
        acc.average = (acc.number * acc.average + aStudent.scroeTotale) / (acc.number + 1)
        acc.number += 1
    }
    
}

print(studentGrade) // (number: 3, average: 17)

五、轉變思想

由於Swift大大簡化了for循環的寫法,所以一般看來其與這些高階函數差別不大。但是,雖然差別不大,這代表這不同的編程範式。我們要勇於接受新的函數式的編程範式,克服命令式編程範式的肌肉記憶。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章