【函式式 Swift】可選值

養樂多發表於2019-02-05

可選值(Optionals)是 Swift 引入的一項非常棒的特性,本文將基於原書中的案例以及函數語言程式設計思想進行討論。


概述

首先推薦大家閱讀原書中的可選值章節,有很多使用細節以及與 Objective-C 的對比討論,我認為如果將這一章單獨拿出來會是一篇講解 Swift 可選值很不錯的文章。

關於 Swift Optionals,我曾寫過一篇《Swift Optionals 原始碼解析》,試圖藉助 Swift 原始碼來解析可選值,其中開篇提到:

Swift 引入的 Optionals,很好的解決了 Objective-C 時代 “nil or not nil” 的問題,配合 Type-Safe 特性,幫我們減少了很多隱藏的問題。

這裡主要有兩方面意思:

  • “nil or not nil”:在 Objective-C 中,nil 是一個極其常見的值,指向一個為空的物件,很重要的是向 nil 發訊息是安全的,回憶一下我們曾經寫過的程式碼,有意無意的都使用了該特性,換句話說,有很多情況我們是不對為空做區別處理的,這樣一來,我們的程式碼通常會簡潔一些,可是一旦由於 nil 的存在或是傳遞導致了 Crash,就不得不去追查任何可能為 nil 的情況。
  • “Type-Safe”:Swift 是一個型別安全的語言,我們需要清楚的瞭解被操作的值的型別,編譯器會對程式碼時進行型別檢查,把不匹配的型別標記為錯誤,這樣能夠幫助我們儘早的發現程式中的問題。而對於類似 Objective-C 中的 nil 來說,一定程度與型別安全產生了衝突,因而,引入 Optionals 來與 Type-Safe 配合很好的解決這一問題。

下面將結合原書案例進一步討論。

注:本文將不涉及對可選值基礎應用的講解,如需學習,請查閱原書官方文件《Swift Optionals 原始碼解析》

案例:Dictionary

使用兩個 Dictionary 分別儲存國家及其首都、城市名及其人口數量:

let capitals = [
    "France": "Paris",
    "Spain": "Madrid",
    "The Netherlands": "Amsterdam",
    "Belgium": "Brussels"
]

let cities = ["Paris": 2241, "Madrid": 3165, "Amsterdam": 827, "Berlin": 3562] // 單位為“千”複製程式碼

問題:編寫函式接收輸入的國家名,輸出其首都城市的人口數量。

與之前章節一樣,問題本身並不複雜,為了對比,我們先編寫一個 Objective-C 版本的函式:

@property (nonatomic, copy) NSDictionary *capitals;
@property (nonatomic, copy) NSDictionary *cities;

_capitals = [[NSDictionary alloc] initWithObjectsAndKeys:
            @"Paris", @"France",
            @"Madrid", @"Spain",
            @"Amsterdam", @"The Netherlands",
            @"Brussels", @"Belgium",
            nil];

_cities = [[NSDictionary alloc] initWithObjectsAndKeys:
            @2241, @"Paris",
            @3165, @"Madrid",
            @827, @"Amsterdam",
            @3562, @"Berlin",
            nil];

- (NSInteger)populationOfCapital:(NSString *)country {
    return [_cities[_capitals[country]] integerValue] * 1000;
}複製程式碼

populationOfCapital 函式就是我們的解決方案,只需要一行程式碼即可,下面測試一下:

// Case 1
NSLog(@"%ld", [self populationOfCapital:@"France"]); // 2241000

// Case 2
NSLog(@"%ld", [self populationOfCapital:@"China"]); // 0複製程式碼

對於 Case 1 一切正常,而對於 Case 2,由於 _capitals 中不包含 China,所以 _capitals[country]nil,按照 Objective-C 的 nil 處理辦法,我們得到了 0,但是,我們無法區分這個是正常的結果還是異常的輸出,如果我們修改一下 _capitals_cities

_capitals = [[NSDictionary alloc] initWithObjectsAndKeys:
            @"Paris", @"France",
            @"Madrid", @"Spain",
            @"Amsterdam", @"The Netherlands",
            @"Brussels", @"Belgium",
            @"Tokyo", @"Japan", // Add Japan
            nil];

_cities = [[NSDictionary alloc] initWithObjectsAndKeys:
            @2241, @"Paris",
            @3165, @"Madrid",
            @827, @"Amsterdam",
            @3562, @"Berlin",
            @0, @"Tokyo", // Add Tokyo
            nil];

// Case 3
NSLog(@"%ld", [self populationOfCapital:@"Japan"]); // 0複製程式碼

我們忽略 Tokyo 是不是真的“零人口”,Case 3 輸出 0 是正確的,但我們無法區分 Case 2 和 Case 3 的輸出。這也就是 “nil or not nil” 問題。

那麼 Swift 如何使用 Optional 解決這個問題呢?我們知道 populationOfCapital 函式應該能夠區分 nilnot nil 的情況,而這正好匹配了可選值的定義:

// Optional.swift(swift/stdlib/public/core/Optional.swift)
public enum Optional<Wrapped> : ExpressibleByNilLiteral {
    case none // nil
    case some(Wrapped) // not nil
}複製程式碼

因此,populationOfCapital 的返回值型別應為 Int?capitalscities 查詢失敗時對應 nil 的情況,查詢成功則對應 not nil,所以 Swift 中 populationOfCapital 函式定義如下:

func populationOfCapital(country: String) -> Int? {
    if let capital = capitals[country] {
        if let population = cities[capital] {
            return population * 1000
        } else {
            return nil
        }
    } else {
        return nil
    }
}

let cities = [
    "Paris": 2241, 
    "Madrid": 3165, 
    "Amsterdam": 827, 
    "Berlin": 3562, 
    "Tokyo": 0
]

let capitals = [
    "France": "Paris",
    "Spain": "Madrid",
    "The Netherlands": "Amsterdam",
    "Belgium": "Brussels",
    "Japan": "Tokyo"
]

// Case 4
populationOfCapital(country: "France") // 2241000
populationOfCapital(country: "China") // nil
populationOfCapital(country: "Japan") // 0複製程式碼

可以看到,藉助可選值,Case 4 的輸出滿足了我們的需求,China 和 Japan 的情況也得到了區分。問題已經解決了,不過 populationOfCapital 完成的不夠“美觀”,也並沒有利用到函數語言程式設計思想,下面對它進行優化。

首先,我們先來看看 populationOfCapital 到底要完成什麼工作,本質上就是從一個值轉變為另一個值,即從國家名轉變為人口數量,只是在轉變過程中可能存在“失敗”的情況,需要對這些“失敗”的情況作出判斷,通過第三章,我們知道值的轉變對應的就是 map 方法,所以我們需要開發的工具庫就是支援可選值的 map 方法,稱之為:flatMap

要為 Optional 新增 flatMap 函式,並且存在“失敗”的情況,所以 flatMap 函式的輸出型別應為 U?,而輸入,則應該是該可選值不為 nil 時所要做的轉換(轉換本身也是一個函式):

extension Optional {
    func flatMap<U>(f: Wrapped -> U?) -> U? {
        guard let x = self else {
            return nil
        }
        return f(x)
    }
}複製程式碼

工具庫就緒後,我們可以改寫 populationOfCapital 函式:

func populationOfCapital_map(country: String) -> Int? {
    return capitals[country].flatMap { capital in
        return cities[capital]
    }.flatMap { population in
        return population * 1000
    }
}

// Case 5
populationOfCapital_map(country: "France") // 2241000
populationOfCapital_map(country: "China") // nil
populationOfCapital_map(country: "Japan") // 0複製程式碼

藉助工具庫,我們不再需要進行多次判斷,而是直接關注“成功”的情況即可。事實上,在 Optional 類庫中已經存在一個同樣功能的 flatMap 函式,開發中直接呼叫即可,其原始碼如下:

// Optional.swift(swift/stdlib/public/core/Optional.swift)
public func flatMap<U>(_ transform: (Wrapped) throws -> U?)
    rethrows -> U? {

    switch self {
        case .some(let y):
            return try transform(y)
        case .none:
            return .none
    }
}複製程式碼

為什麼使用可選值?

原書中詳細討論了為什麼 Swift 使用可選值,大致總結以下兩點:

  1. 安全特性:不同於 Objective-C 預設近似零的處理方式,Swift 通過顯式的可選型別以及型別安全特性,幫助避免由於缺失值而導致的意外崩潰或是邏輯含糊;
  2. 明確的函式簽名:Objective-C 中函式的引數並無是否為空的強制要求,我們在使用時往往也比較模糊,彷彿空和非空均支援(這種情況目前有所改觀,藉助 nullableNS_ASSUME_NONNULL_BEGINNS_ASSUME_NONNULL_END 能夠幫助我們設計空和非空的情況),而由於可選值的存在,使得 Swift 中的函式簽名是否支援 nil 變得非常清晰,更有利於我們寫出健壯的程式碼。

參考資料

  1. Github: objcio/functional-swift
  2. The Swift Programming Language: The Basics
  3. The Swift Programming Language (Source Code)

本文屬於《函式式 Swift》讀書筆記系列,同步更新於 huizhao.win,歡迎關注!

相關文章