一、先理解:iOS 開發中函數式編程的核心原則

函數式編程(FP)在 iOS 開發中的核心是:用純函數、不可變數據、函數組合替代傳統的命令式代碼,核心原則:

  1. 純函數:輸入決定輸出,無副作用(不修改外部變量、不依賴全局狀態、不觸發 UI 刷新);
  2. 不可變數據:優先用let而非var,避免數據被意外修改(適配 iOS 多線程安全);
  3. 函數是一等公民:函數可作為參數、返回值,支持組合 / 柯里化;
  4. 聲明式編程:關注 “做什麼” 而非 “怎麼做”(如用map替代for循環)。

Swift 天生支持這些特性(閉包、高階函數、泛型、值類型),無需第三方庫即可落地函數式編程。


二、Swift 中函數式編程的核心工具(iOS 開發高頻用)

1. 高階函數(替代命令式循環)

Swift 的map/filter/reduce/compactMap/flatMap是 iOS 開發中最常用的函數式工具,適用於數據處理(如列表過濾、模型轉換):

示例 1:數據模型轉換(替代 for 循環)

swift

// 命令式(傳統寫法:修改外部變量,有副作用)
var userNameList: [String] = []
for user in userModels {
    if let name = user.name { // 判空
        userNameList.append(name)
    }
}

// 函數式(純函數,無副作用,一行完成)
let userNameList = userModels.compactMap { $0.name } // compactMap自動過濾nil
示例 2:過濾 + 轉換 + 聚合(iOS 列表數據處理)

swift

// 需求:篩選年齡>18的用户,提取暱稱,拼接成逗號分隔的字符串
let userModels: [UserModel] = [/* 測試數據 */]

// 命令式(多層循環+變量修改)
var filteredNames: [String] = []
for user in userModels {
    if user.age > 18, let name = user.nickname {
        filteredNames.append(name)
    }
}
let nameString = filteredNames.joined(separator: ", ")

// 函數式(聲明式,無外部變量)
let nameString = userModels
    .filter { $0.age > 18 } // 過濾:年齡>18
    .compactMap { $0.nickname } // 轉換:提取非空暱稱
    .joined(separator: ", ") // 聚合:拼接字符串
示例 3:reduce(數據聚合,如計算總價)

swift

// 需求:計算購物車中已選中商品的總價
let cartItems: [CartItem] = [/* 測試數據 */]

// 函數式實現
let totalPrice = cartItems
    .filter { $0.isSelected } // 過濾選中商品
    .reduce(0.0) { $0 + $1.price * Double($1.quantity) } // 累加:初始值0,$0是累計值,$1是當前項

2. 純函數(無副作用,可複用)

iOS 開發中,將業務邏輯(如數據驗證、格式轉換)封裝為純函數,避免耦合 UI / 全局狀態:

swift

// 純函數:手機號格式化(輸入決定輸出,無副作用)
func formatPhoneNumber(_ phone: String) -> String {
    // 僅處理輸入,不依賴外部變量、不修改UI
    let digits = phone.filter { $0.isNumber } // 提取數字
    guard digits.count == 11 else { return phone }
    return "(\(digits.prefix(3))) \(digits.dropFirst(3).prefix(4)) \(digits.suffix(4))"
}

// 使用(可在VC/VM/工具類中複用,無耦合)
let formattedPhone = formatPhoneNumber("13800138000")
// 輸出:(138) 0013 8000

3. 函數組合(將小函數組合成大函數)

將多個純函數組合,實現複雜邏輯,iOS 中適用於表單驗證、數據解析等場景:

swift

// 定義組合運算符(Swift支持自定義運算符)
infix operator >>>: AdditionPrecedence
func >>> <T, U, V>(_ f: @escaping (T) -> U, _ g: @escaping (U) -> V) -> (T) -> V {
    return { g(f($0)) }
}

// 小函數1:去除字符串空格
func trim(_ str: String) -> String {
    return str.trimmingCharacters(in: .whitespaces)
}

// 小函數2:驗證字符串非空
func validateNonEmpty(_ str: String) -> String? {
    return str.isEmpty ? nil : str
}

// 小函數3:轉換為郵箱格式(簡單驗證)
func validateEmail(_ str: String) -> String? {
    let pattern = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$"
    return NSPredicate(format: "SELF MATCHES %@", pattern).evaluate(with: str) ? str : nil
}

// 組合成:表單郵箱驗證函數
let validateFormEmail = trim >>> validateNonEmpty >>> validateEmail

// 使用(iOS表單提交場景)
let email = "  test@example.com  "
if let validEmail = validateFormEmail(email) {
    print("郵箱有效:\(validEmail)")
} else {
    print("郵箱格式錯誤")
}

4. 不可變數據(避免意外修改)

iOS 開發中優先用let、結構體(值類型)、不可變集合,替代可變數據:

swift

// 命令式(可變數組,易被意外修改)
var mutableUsers = [UserModel]()
mutableUsers.append(UserModel(id: "1", name: "張三"))
mutableUsers[0].name = "李四" // 意外修改

// 函數式(不可變數組,修改需生成新數組)
let immutableUsers = [UserModel(id: "1", name: "張三")]
// 如需修改,生成新數組(原數組不變)
let updatedUsers = immutableUsers.map { user in
    var newUser = user
    newUser.name = "李四"
    return newUser
}

三、iOS 開發中函數式編程的具體場景落地

1. UIKit 控件配置(函數式封裝,替代 Then 庫)

無需第三方庫,用函數式思想封裝 UI 配置,實現鏈式調用:

swift

// 函數式封裝UIButton配置
extension UIButton {
    // 純函數:配置屬性,返回自身(支持鏈式)
    func withTitle(_ title: String, for state: UIControl.State = .normal) -> Self {
        setTitle(title, for: state)
        return self
    }
    
    func withTitleColor(_ color: UIColor, for state: UIControl.State = .normal) -> Self {
        setTitleColor(color, for: state)
        return self
    }
    
    func withBackgroundColor(_ color: UIColor) -> Self {
        backgroundColor = color
        return self
    }
    
    func withCornerRadius(_ radius: CGFloat) -> Self {
        layer.cornerRadius = radius
        clipsToBounds = true
        return self
    }
}

// 使用(聲明式配置,無外部變量)
let submitButton = UIButton()
    .withTitle("提交")
    .withTitleColor(.white)
    .withBackgroundColor(.systemBlue)
    .withCornerRadius(8)
    .withFrame(CGRect(x: 20, y: 100, width: 300, height: 44))

2. 網絡請求 + 數據解析(函數式鏈式處理)

將網絡請求、解析、錯誤處理封裝為純函數,避免回調嵌套(Callback Hell):

swift

// 定義類型別名,簡化函數簽名
typealias NetworkRequest<T: Decodable> = () -> Result<T, NetworkError>
typealias DataParser<T: Decodable> = (Data) -> Result<T, NetworkError>

// 純函數:網絡請求
func fetchData<T: Decodable>(url: URL) -> NetworkRequest<T> {
    return {
        do {
            let (data, _) = try URLSession.shared.data(from: url)
            return .success(data as! T) // 簡化示例,實際需解析
        } catch {
            return .failure(.networkError)
        }
    }
}

// 純函數:JSON解析
func parseJSON<T: Decodable>() -> DataParser<T> {
    return { data in
        do {
            let model = try JSONDecoder().decode(T.self, from: data)
            return .success(model)
        } catch {
            return .failure(.parseError)
        }
    }
}

// 組合:請求+解析
func fetchAndParse<T: Decodable>(url: URL) -> Result<T, NetworkError> {
    let requestResult = fetchData(url: url)()
    switch requestResult {
    case .success(let data):
        return parseJSON<T>()(data as! Data)
    case .failure(let error):
        return .failure(error)
    }
}

// 使用(iOS 15+可結合async/await)
Task {
    let url = URL(string: "https://api.example.com/users")!
    let result: Result<[UserModel], NetworkError> = fetchAndParse(url: url)
    switch result {
    case .success(let users):
        print("獲取用户:\(users.count)")
    case .failure(let error):
        print("錯誤:\(error)")
    }
}

3. 響應式 UI 更新(結合 Combine,函數式響應式)

Swift 的 Combine 框架(iOS 13+)是函數式編程在 iOS 響應式開發的核心,適用於數據驅動 UI:

swift

import Combine

class UserVM {
    // 不可變數據源(@Published自動發佈變化)
    @Published private(set) var userList: [UserModel] = []
    private var cancellables = Set<AnyCancellable>()
    
    // 函數式:請求數據+解析+更新UI(無副作用)
    func loadUsers() {
        let url = URL(string: "https://api.example.com/users")!
        URLSession.shared.dataTaskPublisher(for: url)
            .map { $0.data } // 提取數據
            .decode(type: [UserModel].self, decoder: JSONDecoder()) // 解析
            .replaceError(with: []) // 錯誤處理:返回空數組
            .receive(on: DispatchQueue.main) // 切換到主線程
            .assign(to: \.userList, on: self) // 賦值給數據源
            .store(in: &cancellables)
    }
}

// VC中綁定UI(函數式響應式)
class UserVC: UIViewController {
    let vm = UserVM()
    let tableView = UITableView()
    private var cancellables = Set<AnyCancellable>()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // 監聽數據源變化,自動刷新表格(聲明式,無需手動調用reloadData)
        vm.$userList
            .sink { [weak self] _ in
                self?.tableView.reloadData()
            }
            .store(in: &cancellables)
        
        vm.loadUsers()
    }
}

四、iOS 開發中函數式編程的最佳實踐與避坑

1. 最佳實踐

  • 小而純:函數只做一件事(如 “格式化手機號” 而非 “格式化 + 驗證 + 提交”);
  • 優先值類型:數據模型用struct,避免引用類型的副作用;
  • 避免過度抽象:簡單邏輯(如單次循環)無需強行用高階函數,可讀性優先;
  • 結合 iOS 特性:函數式代碼需適配 iOS 主線程、AutoLayout、權限等,不可脱離平台;
  • 錯誤處理:用Result類型替代 try-catch 嵌套,統一錯誤處理邏輯。

2. 避坑點

  • 性能問題:多層高階函數(如mapfilterreduce)會遍歷多次數組,大數據量時可合併為單次遍歷;
  • 調試難度:函數式代碼的錯誤棧較抽象,可通過拆分小函數、添加日誌解決;
  • UI 副作用:純函數不可直接修改 UI,需通過 Combine / 閉包將結果傳遞到 UI 層;
  • 兼容低版本:Combine 僅支持 iOS 13+,低版本可使用第三方庫(如 RxSwift)或簡化的函數式封裝。

總結

  1. 核心工具:Swift 的高階函數(map/filter/reduce)、純函數、函數組合是 iOS 函數式編程的基礎;
  2. 核心場景:數據處理、UI 配置、網絡請求 / 解析、響應式 UI 更新(Combine)是 iOS 中函數式編程的高頻落地場景;
  3. 核心原則:保持函數純、數據不可變、聲明式編程,同時兼顧 iOS 平台特性(主線程、版本兼容、UI 副作用)。

函數式編程在 iOS 開發中的價值是降低代碼耦合、提升複用性、減少副作用,無需追求 “純函數式”,而是結合命令式編程的優勢,在核心業務邏輯中落地函數式思想即可。