優雅的對 Optional Value 進行 String Interpolation

Source: SPY×FAMILY.CA4LA

String Interpolation 是組合字串時很常用的方式,可以很直接的把字串組合出來的結果顯示出來,例如:

var age: Int = 25

"Your age is \(age)."

對 Optional Value 進行 String Interpolation

但很多時候,要組合的變數是 optional 的,例如上面例子的 age,如果改成 Int?,就會收到這樣的 Warning:

String interpolation produces a debug description for an optional value; did you mean to make this explicit?

而組合出來的結果會變成:

Your age is Optional(25).

要避免這種情形,就必須先對 age 進行 unwrap,例如:

var age: Int? = 25

"Your age is \(age ?? 0)."

但這種寫法,如果遇到 age 真的是 nil 時,組合出來的結果就會變成:

Your age is 0.

這種情況通常不是我們想要的,我們會希望如果使用者沒有提供年紀,那就用個佔位符取代。你可能會想這樣寫:

"Your age is \(age ?? "top secret")."

這次不會收到 Warning,而是 Error:

Binary operator '??' cannot be applied to operands of type 'Int?' and 'String'

於是為了解決這問題,我們會看到各式各樣的寫法,但基本的方向都是不直接 interpolate Int? 這個 type,而是先轉換成 String 再操作,一個常見的方法可能是像下面這種寫法:

let ageString = {
    guard let age = age else {
        return "top secret"
    }

    return "\(age)"
}()

"Your age is \(ageString)."

我們用了 letageString 不會在賦值後又意外被修改。用了 closure 來讓 ageString 的運算獨立成一個區塊,避免 scope 的污染。用了 guard 來做 early return。

不醜,但可以更好。

ExpressibleByStringInterpolation 協定

我們回頭來看一下,String 之所以可以用 String interpolation 來建構,是因為 String 遵循了 ExpressibleByStringInterpolation 協定,而 ExpressibleByStringInterpolation 定義了一個 associatedtype StringInterpolation,這才是實際負責處理插值的地方,我們可以透過擴充 StringInterpolation,來改變插值的行為,例如增加對自訂 type 的支援,或者也可以增加一些參數。

Swift 中提供了一個 DefaultStringInterpolation 作為 String/Substring 的預設實作,所以我們的目標就是:擴充這個 struct 來增加對 optional value 的處理能力

我們的第一版實作如下:

extension DefaultStringInterpolation {
    mutating func appendInterpolation<T>(_ value: T?,
                                         placeholder: @autoclosure () -> String)
    {
        switch value {
        case .some(let value):
            appendInterpolation(value)

        default:
            appendInterpolation(placeholder())
        }
    }
}

這樣一來,我們就可以這樣寫:

"Your age is \(age, placeholder: "top secret")."

是不是瞬間美麗了起來?

但有時除了 nil 需要顯示佔位符之外,我們還需要對數值做些檢查,例如是否 > 0,或者字串是否為空,所以我們可以再一次擴充這個實作:

extension DefaultStringInterpolation {
    mutating func appendInterpolation<T>(_ value: T?,
                                         placeholder: @autoclosure () -> String,
                                         where predicate: ((T) throws -> Bool)? = nil) rethrows
    {
        switch value {
        case .some(let value) where try predicate?(value) != false:
            appendInterpolation(value)

        default:
            appendInterpolation(placeholder())
        }
    }
}

呼叫的時候則可以寫成:

"Your age is \(age, placeholder: "top secret", where: { $0 > 0 })."

至此,我們就可以說聲:優雅。

Ref:

Contents

comments powered by Disqus