實用的可選項(Optional)擴充套件

SwiftGG翻譯組發表於2018-11-19

作者:terhechte,原文連結,原文日期:2018-01-10譯者:rsenjoyer;校對:numbbbbbYousanflics;定稿:Forelax

可選值(Optional)是 Swift 語言最基礎的內容。我想每個人都同意它帶來了巨大的福音,因為它迫使開發者妥善處理邊緣情況。可選值的語言特效能讓發者在開發階段發現並處理整個類別的 bug。

然而,Swift 標準庫中可選值的 API 相當的有限。如果忽略 customMirrordebugDescription 屬性,Swift 文件 僅僅列出了幾個方法/屬性:

var unsafelyUnwrapped: Wrapped { 
get
} func map<
U>
(_ transform: (Wrapped)
throws ->
U) rethrows ->
U? func flatMap<
U>
(_ transform: (Wrapped)
throws ->
U?) rethrows ->
U?複製程式碼

即使方法如此少,可選值仍然非常有用,這是因為 Swift 在語法上通過 可選鏈模式匹配if letguard let 等功能來彌補它。但在某些情況下,可選值容易造成多分支條件。有時,一個非常簡潔的方法通常允許你用一行程式碼表達某個概念,而不是用多行組合的 if let 語句。

我篩選了 Github 上的 Swift 專案以及 Rust、Scala 或 C# 等其他語言的可選實現,目的是為 Optional 找一些有用的補充。以下 14 個可選擴充套件,我將分類逐一解釋,同時給每個類別舉幾個例子。最後,我將編寫一個更復雜的示例,它同時使用多個可選擴充套件。

判空(Emptiness)

extension Optional { 
/// 可選值為空的時候返回 true var isNone: Bool {
switch self {
case .none: return true case .some: return false
}
} /// 可選值非空返回 true var isSome: Bool {
return !isNone
}
}複製程式碼

這是對可選型別最基礎的補充。我很喜歡這些補充,因為它們將可選項為空的概念從程式碼中移除了。在使用的細節上, 使用 optional.isSomeif optional == nil 更簡潔明瞭。

// 使用前guard leftButton != nil, rightButton != nil else { 
fatalError("Missing Interface Builder connections")
}// 使用後guard leftButton.isSome, rightButton.isSome else {
fatalError("Missing Interface Builder connections")
}複製程式碼

或(Or)

extension Optional { 
/// 返回可選值或預設值 /// - 引數: 如果可選值為空,將會預設值 func or(_ default: Wrapped) ->
Wrapped {
return self ?? `default`
} /// 返回可選值或 `else` 表示式返回的值 /// 例如. optional.or(else: print("Arrr")) func or(else: @autoclosure () ->
Wrapped) ->
Wrapped {
return self ?? `else`()
} /// 返回可選值或者 `else` 閉包返回的值 // 例如. optional.or(else: {
/// ... do a lot of stuff ///
})
func or(else: () ->
Wrapped) ->
Wrapped {
return self ?? `else`()
} /// 當可選值不為空時,返回可選值 /// 如果為空,丟擲異常 func or(throw exception: Error) throws ->
Wrapped {
guard let unwrapped = self else {
throw exception
} return unwrapped
}
}extension Optional where Wrapped == Error {
/// 當可選值不為空時,執行 `else` func or(_ else: (Error) ->
Void) {
guard let error = self else {
return
} `else`(error)
}
}複製程式碼

isNone / isSome 的另一個抽象概念是能夠指定當變數不成立的時需要執行的指令。這能讓我們避免編寫 ifguard 分支,而是將邏輯封裝為一個易於理解的方法。

這個概念非常的有用,它可在四個不同功能中被定義。

預設值(Default Value)

第一個擴充套件方法是返回可選值或者預設值:

let optional: Int? = nilprint(optional.or(10)) // 列印 10複製程式碼

預設閉包(Default Closure)

預設閉包和預設值非常的相似,但它允許從閉包中返回預設值。

let optional: Int? = niloptional.or(else: secretValue * 32)複製程式碼

由於使用了 @autoclosure 引數, 我們實際上使用的是預設閉包。使用預設值將會自動轉換為返回值的閉包。然而,我傾向於將兩個實現單獨分開,因為它可以讓使用者用更加複雜的邏輯編寫閉包。

let cachedUserCount: Int? = nil...return cachedUserCount.or(else: { 
let db = database() db.prefetch() guard db.failures.isEmpty else {
return 0
} return db.amountOfUsers
})複製程式碼

當你對一個為空的可選值賦值的時候,使用 or 就是一個不錯的選擇。

if databaseController == nil { 
databaseController = DatabaseController(config: config)
}複製程式碼

上面的程式碼可以寫的更加優雅:

databaseController = databaseController.or(DatabaseController(config: config)複製程式碼

丟擲異常(Throw an error)

這也是一個非常有用的補充,因為它將 Swift 中可選值與錯誤處理連線起來。根據專案中的程式碼,方法或函式通過返回一個為空的可選值(例如訪問字典中不存在的鍵)時,丟擲錯誤來表述這一無效的行為。將兩者連線起來能夠使程式碼更加清晰:

func buildCar() throws ->
Car {
let tires = try machine1.createTires() let windows = try machine2.createWindows() guard let motor = externalMachine.deliverMotor() else {
throw MachineError.motor
} let trunk = try machine3.createTrunk() if let car = manufacturer.buildCar(tires, windows, motor, trunk) {
return car
} else {
throw MachineError.manufacturer
}
}複製程式碼

在這個例子中,我們通過呼叫內部及外部程式碼共同構建汽車物件,外部程式碼(external_machinemanufacturer)選擇使用可選值而不是錯誤處理。這使得程式碼變得很複雜,我們可使用 or(throw:) 使函式可讀性更高。

func build_car() throws ->
Car {
let tires = try machine1.createTires() let windows = try machine2.createWindows() let motor = try externalMachine.deliverMotor().or(throw: MachineError.motor) let trunk = try machine3.createTrunk() return try manufacturer.buildCar(tires, windows, motor, trunk).or(throw: MachineError.manufacturer)
}複製程式碼

錯誤處理(Handling Errors)

當程式碼中包含 Stijn Willems 在 Github 自由函式,上面丟擲異常部分的程式碼變更加有用。感謝 Stijn Willems 的建議。

func should(_ do: () throws ->
Void) ->
Error? {
do {
try `do`() return nil
} catch let error {
return error
}
}複製程式碼

這個自由函式(可選的,可將它當做一個可選項的類方法)使用 do {
} catch {
}
塊並返回一個錯誤。當且僅當 do 程式碼塊捕捉到異常。以下面 Swift 程式碼為例:

do { 
try throwingFunction()
} catch let error {
print(error)
}複製程式碼

這是 Swift 中錯誤處理的基本原則之一,但它不夠簡單明瞭。使用上面的提供的函式,你可以使程式碼變得足夠簡單。

should { 
try throwingFunction)
}.or(print($0))複製程式碼

我覺得在很多情況下,這樣進行錯誤處理效果更好。

變換(Map)

正如上面所見,mapflatMap 是 Swift 標準庫在可選項上面提供的的全部方法。然而,在多數情況下,也可以對它們稍微改進使得更加通用。這有兩個擴充套件 map 允許定義一個預設值,類似於上面 or 的實現方式:

extension Optional { 
/// 可選值變換返回,如果可選值為空,則返回預設值 /// - 引數 fn: 對映值的閉包 /// - 引數 default: 可選值為空時,將作為返回值 func map<
T>
(_ fn: (Wrapped)
throws ->
T, default: T) rethrows ->
T {
return try map(fn) ?? `default`
} /// 可選值變換返回,如果可選值為空,則呼叫 `else` 閉包 /// - 引數 fn: 對映值的閉包 /// - 引數 else: The function to call if the optional is empty func map<
T>
(_ fn: (Wrapped)
throws ->
T, else: () throws ->
T) rethrows ->
T {
return try map(fn) ?? `else`()
}
}複製程式碼

第一個方法允許你將可選值 map 成一個新的型別 T. 如果可選值為空,你可以提供一個 T 型別的預設值:

let optional1: String? = "appventure"let optional2: String? = nil// 使用前print(optional1.map({ 
$0.count
}) ?? 0)print(optional2.map({
$0.count
}) ?? 0)// 使用後 print(optional1.map({
$0.count
}, default: 0)) // prints 10print(optional2.map({
$0.count
}, default: 0)) // prints 0複製程式碼

這裡改動很小,我們再也不需要使用 ?? 操作符,取而代之的是更能表達意圖的 default 值。

第二個方法也與第一個很相似,主要區別在於它接受(再次)返回 T 型別的閉包,而不是使用一個預設值。這裡有個簡單的例子:

let optional: String? = nilprint(optional.map({ 
$0.count
}, else: {
"default".count
})複製程式碼

組合可選項(Combining Optionals)

這個類別包含了四個函式,允許你定義多個可選項之間的關係。

extension Optional { 
/// 當可選值不為空時,解包並返回引數 `optional` func and<
B>
(_ optional: B?)
->
B? {
guard self != nil else {
return nil
} return optional
} /// 解包可選值,當可選值不為空時,執行 `then` 閉包,並返回執行結果 /// 允許你將多個可選項連線在一起 func and<
T>
(then: (Wrapped)
throws ->
T?) rethrows ->
T? {
guard let unwrapped = self else {
return nil
} return try then(unwrapped)
} /// 將當前可選值與其他可選值組合在一起 /// 當且僅當兩個可選值都不為空時組合成功,否則返回空 func zip2<
A>
(with other: Optional<
A>
)
->
(Wrapped, A)? {
guard let first = self, let second = other else {
return nil
} return (first, second)
} /// 將當前可選值與其他可選值組合在一起 /// 當且僅當三個可選值都不為空時組合成功,否則返回空 func zip3<
A, B>
(with other: Optional<
A>
, another: Optional<
B>
)
->
(Wrapped, A, B)? {
guard let first = self, let second = other, let third = another else {
return nil
} return (first, second, third)
}
}複製程式碼

上面的四個函式都以傳入可選值當做引數,最終都返回一個可選值,然而,他們的實現方式完全不同。

依賴(Dependencies)

若一個可選值的解包僅作為另一可選值解包的前提,and<
B>
(_ optional)
就顯得非常使用:

// 使用前if user != nil, let account = userAccount() ...// 使用後if let account = user.and(userAccount()) ...複製程式碼

在上面的例子中,我們對 user 的具體內容不感興趣,但是要求在呼叫 userAccount 函式前保證它非空。雖然這種關係也可以使用 user != nil,但我覺得 and 使它們的意圖更加清晰。

鏈式呼叫(Chaining)

and<
T>
(then:)
是另一個非常有用的函式, 它將多個可選項鍊接起來,以便將可選項 A 的解包值當做可選項 B 的輸入。我們從一個簡單的例子開始:

protocol UserDatabase { 
func current() ->
User? func spouse(of user: User) ->
User? func father(of user: User) ->
User? func childrenCount(of user: User) ->
Int
}let database: UserDatabase = ...// 思考如下關係該如何表達:// Man ->
Spouse ->
Father ->
Father ->
Spouse ->
children
// 使用前let childrenCount: Intif let user = database.current(), let father1 = database.father(user), let father2 = database.father(father1), let spouse = database.spouse(father2), let children = database.childrenCount(father2) {
childrenCount = children
} else {
childrenCount = 0
}// 使用後let children = database.current().and(then: {
database.spouse($0)
}) .and(then: {
database.father($0)
}) .and(then: {
database.spouse($0)
}) .and(then: {
database.childrenCount($0)
}) .or(0)複製程式碼

使用 and(then) 函式對程式碼有很大的提升。首先,你沒必要宣告臨時變數名(user, father1, father2, spouse, children),其次,程式碼更加的簡潔。而且,使用 or(0)let childrenCount 可讀性更好。

最後,原來的 Swift 程式碼很容易導致邏輯錯誤。也許你還沒有注意到,但示例中存在一個 bug。在寫那樣的程式碼時,就很容易地引入複製貼上錯誤。你觀察到了麼?

是的,children 屬性應該由呼叫 database.childrenCount(spouse) 建立,但我寫成了 database.childrenCount(father2)。很難發現這樣的錯誤。使用 and(then:) 就容易發現這個錯誤,因為它使用的是變數 $0

組合(Zipping)

這是現有 Swift 概念的另一個擴充套件,zip 可以組合多個可選值,它們一起解包成功或解包失敗。在上面的程式碼片段中,我提供了 zip2zip3 函式,但你也可以命名為 zip22(好吧,也許對合理性和編譯速度有一點點影響)。

// 正常示例func buildProduct() ->
Product? {
if let var1 = machine1.makeSomething(), let var2 = machine2.makeAnotherThing(), let var3 = machine3.createThing() {
return finalMachine.produce(var1, var2, var3)
} else {
return nil
}
}// 使用擴充套件func buildProduct() ->
Product? {
return machine1.makeSomething() .zip3(machine2.makeAnotherThing(), machine3.createThing()) .map {
finalMachine.produce($0.1, $0.2, $0.3)
}
}複製程式碼

程式碼量更少,程式碼更清晰,更優雅。然而,也存一個缺點,就是更復雜了。讀者必須瞭解並理解 zip 才能完全掌握它。

On

extension Optional { 
/// 當可選值不為空時,執行 `some` 閉包 func on(some: () throws ->
Void) rethrows {
if self != nil {
try some()
}
} /// 當可選值為空時,執行 `none` 閉包 func on(none: () throws ->
Void) rethrows {
if self == nil {
try none()
}
}
}複製程式碼

不論可選值是否為空,上面兩個擴充套件都允許你執行一些額外的操作。與上面討論過的方法相反,這兩個方法忽略可選值。on(some:) 會在可選值不為空的時候執行閉包 some,但是閉包 some 不會獲取可選項的值。

/// 如果使用者不存在將登出self.user.on(none: { 
AppCoordinator.shared.logout()
})/// 當使用者不為空時,連線網路self.user.on(some: {
AppCoordinator.shared.unlock()
})複製程式碼

Various

extension Optional { 
/// 可選值不為空且可選值滿足 `predicate` 條件才返回,否則返回 `nil` func filter(_ predicate: (Wrapped) ->
Bool) ->
Wrapped? {
guard let unwrapped = self, predicate(unwrapped) else {
return nil
} return self
} /// 可選值不為空時返回,否則 crash func expect(_ message: String) ->
Wrapped {
guard let value = self else {
fatalError(message)
} return value
}
}複製程式碼

過濾(Filter)

這個方法類似於一個守護者一樣,只有可選值滿足 predicate 條件時才進行解包。比如說,我們希望所有的老使用者都升級為高階賬戶,以便與我們保持更長久的聯絡。

// 僅會影響 id <
1000 的使用者
// 正常寫法if let aUser = user, user.id <
1000 {
aUser.upgradeToPremium()
}// 使用 `filter`user.filter({
$0.id <
1000
})?.upgradeToPremium()複製程式碼

在這裡,user.filter 使用起來更加自然。此外,它的實現類似於 Swift 集合中的功能。

期望(Expect)

這是我最喜歡的功能之一。這是我從 Rush 語言中借鑑而來的。我試圖避免強行解包程式碼庫中的任何東西。類似於隱式解包可選項。

然而,當在專案中使用視覺化介面構建 UI 時,下面的這種方式很常見:

func updateLabel() { 
guard let label = valueLabel else {
fatalError("valueLabel not connected in IB")
} label.text = state.title
}複製程式碼

顯然,另一種方式是強制解包 label, 這麼做可能會造成應用程式崩潰類似於 fatalError。 然而,我必須插入 !, 當造成程式崩潰後,! 並不能給明確的錯誤資訊。在這裡,使用上面實現的 expect 函式就是一個更好的選擇:

func updateLabel() { 
valueLabel.expect("valueLabel not connected in IB").text = state.title
}複製程式碼

示例(Example)

至此我們已經實現了一系列非常有用的可選項擴充套件。我將會給出個綜合示例,以便更好的瞭解如何組合使用這些擴充套件。首先,我們需要先說明一下這個示例,原諒我使用這個不太恰當的例子:

假如你是為 80 年代的軟體商工作。每個月都有很多的人為你編寫應用軟體和遊戲。你需要追蹤銷售量,你從會計那裡收到一個 XML 檔案,你需要進行解析並將結果存入到資料庫中(如果在 80 年代就有 Swift 語言 以及 XML,這將是多麼奇妙)。你的軟體系統有一個XML解析器和一個資料庫(當然都是用6502 ASM編寫的),它們實現了以下協議:

protocol XMLImportNode { 
func firstChild(with tag: String) ->
XMLImportNode? func children(with tag: String) ->
[XMLImportNode] func attribute(with name: String) ->
String?
}typealias DatabaseUser = Stringtypealias DatabaseSoftware = Stringprotocol Database {
func user(for id: String) throws ->
DatabaseUser func software(for id: String) throws ->
DatabaseSoftware func insertSoftware(user: DatabaseUser, name: String, id: String, type: String, amount: Int) throws func updateSoftware(software: DatabaseSoftware, amount: Int) throws
}複製程式碼

XML 檔案可能看起來像這樣:

<
users>
<
user name="" id="158">
<
software>
<
package type="game" name="Maniac Mansion" id="4332" amount="30" />
<
package type="game" name="Doom" id="1337" amount="50" />
<
package type="game" name="Warcraft 2" id="1000" amount="10" />
<
/software>
<
/user>
<
/users>
複製程式碼

解析 XML 的程式碼如下:

enum ParseError: Error { 
case msg(String)
}func parseGamesFromXML(from root: XMLImportNode, into database: Database) throws {
guard let users = root.firstChild(with: "users")?.children(with: "user") else {
throw ParseError.msg("No Users")
} for user in users {
guard let software = user.firstChild(with: "software")? .children(with: "package"), let userId = user.attribute(with: "id"), let dbUser = try? database.user(for: userId) else {
throw ParseError.msg("Invalid User")
} for package in software {
guard let type = package.attribute(with: "type"), type == "game", let name = package.attribute(with: "name"), let softwareId = package.attribute(with: "id"), let amountString = package.attribute(with: "amount") else {
throw ParseError.msg("Invalid Package")
} if let existing = try? database.software(for: softwareId) {
try database.updateSoftware(software: existing, amount: Int(amountString) ?? 0)
} else {
try database.insertSoftware(user: dbUser, name: name, id: softwareId, type: type, amount: Int(amountString) ?? 0)
}
}
}
}複製程式碼

讓我們運用下上面學到的內容:

func parseGamesFromXML(from root: XMLImportNode, into database: Database) throws { 
for user in try root.firstChild(with: "users") .or(throw: ParseError.msg("No Users")).children(with: "user") {
let dbUser = try user.attribute(with: "id") .and(then: {
try? database.user(for: $0)
}) .or(throw: ParseError.msg("Invalid User")) for package in (user.firstChild(with: "software")? .children(with: "package")).or([]) {
guard (package.attribute(with: "type")).filter({
$0 == "game"
}).isSome else {
continue
} try package.attribute(with: "name") .zip3(with: package.attribute(with: "id"), another: package.attribute(with: "amount")) .map({
(tuple) ->
Void in switch try? database.software(for: tuple.1) {
case let e?: try database.updateSoftware(software: e, amount: Int(tuple.2).or(0)) default: try database.insertSoftware(user: dbUser, name: tuple.0, id: tuple.1, type: "game", amount: Int(tuple.2).or(0))
}
}, or: {
throw ParseError.msg("Invalid Package")
})
}
}
}複製程式碼

如果我們對比下,至少會有兩點映入眼簾:

  1. 程式碼量更少
  2. 程式碼看起來更復雜了

在組合使用可選擴充套件時,我故意造成一種過載狀態。其中的一部分使用很恰當,但是另一部分卻不那麼合適。然而,使用擴充套件的關鍵不在於過度依賴(正如我上面做的那樣),而在於這些擴充套件是否使語義更加清晰明瞭。比較上面的兩個實現方式,在第二個實現中,考慮下是使用 Swift 本身提供的功能好還是使用可選擴充套件更佳。

這就是本文的全部內容,感謝閱讀!

本文由 SwiftGG 翻譯組翻譯,已經獲得作者翻譯授權,最新文章請訪問 swift.gg

來源:https://juejin.im/post/5bf2627ee51d45686a68a245

相關文章