做者:terhechte,原文連接,原文日期:2018-01-10 譯者:rsenjoyer;校對:numbbbbb,Yousanflics;定稿:Forelaxgit
可選值(Optional)是 Swift 語言最基礎的內容。我想每一個人都贊成它帶來了巨大的福音,由於它迫使開發者妥善處理邊緣狀況。可選值的語言特性能讓發者在開發階段發現並處理整個類別的 bug。github
然而,Swift 標準庫中可選值的 API 至關的有限。若是忽略 customMirror
和 debugDescription
屬性,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 let
或 guard let
等功能來彌補它。但在某些狀況下,可選值容易形成多分支條件。有時,一個很是簡潔的方法一般容許你用一行代碼表達某個概念,而不是用多行組合的 if let
語句。swift
我篩選了 Github 上的 Swift 項目以及 Rust、Scala 或 C# 等其餘語言的可選實現,目的是爲 Optional 找一些有用的補充。如下 14 個可選擴展,我將分類逐一解釋,同時給每一個類別舉幾個例子。最後,我將編寫一個更復雜的示例,它同時使用多個可選擴展。windows
extension Optional {
/// 可選值爲空的時候返回 true
var isNone: Bool {
switch self {
case .none:
return true
case .some:
return false
}
}
/// 可選值非空返回 true
var isSome: Bool {
return !isNone
}
}
複製代碼
這是對可選類型最基礎的補充。我很喜歡這些補充,由於它們將可選項爲空的概念從代碼中移除了。在使用的細節上, 使用 optional.isSome
比 if optional == nil
更簡潔明瞭。網絡
// 使用前
guard leftButton != nil, rightButton != nil else { fatalError("Missing Interface Builder connections") }
// 使用後
guard leftButton.isSome, rightButton.isSome else { fatalError("Missing Interface Builder connections") }
複製代碼
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
的另外一個抽象概念是可以指定當變量不成立的時須要執行的指令。這能讓咱們避免編寫 if
或 guard
分支,而是將邏輯封裝爲一個易於理解的方法。閉包
這個概念很是的有用,它可在四個不一樣功能中被定義。app
第一個擴展方法是返回可選值或者默認值:函數
let optional: Int? = nil
print(optional.or(10)) // 打印 10
複製代碼
默認閉包和默認值很是的類似,但它容許從閉包中返回默認值。性能
let optional: Int? = nil
optional.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)
複製代碼
這也是一個很是有用的補充,由於它將 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_machine
和 manufacturer
)選擇使用可選值而不是錯誤處理。這使得代碼變得很複雜,咱們可以使用 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)
}
複製代碼
當代碼中包含 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
和 flatMap
是 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 10
print(optional2.map({ $0.count }, default: 0)) // prints 0
複製代碼
這裏改動很小,咱們不再須要使用 ??
操做符,取而代之的是更能表達意圖的 default
值。
第二個方法也與第一個很類似,主要區別在於它接受(再次)返回 T
類型的閉包,而不是使用一個默認值。這裏有個簡單的例子:
let optional: String? = nil
print(optional.map({ $0.count }, else: { "default".count })
複製代碼
這個類別包含了四個函數,容許你定義多個可選項之間的關係。
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)
}
}
複製代碼
上面的四個函數都以傳入可選值當作參數,最終都返回一個可選值,然而,他們的實現方式徹底不一樣。
若一個可選值的解包僅做爲另外一可選值解包的前提,and<B>(_ optional)
就顯得很是使用:
// 使用前
if user != nil, let account = userAccount() ...
// 使用後
if let account = user.and(userAccount()) ...
複製代碼
在上面的例子中,咱們對 user
的具體內容不感興趣,可是要求在調用 userAccount
函數前保證它非空。雖然這種關係也可使用 user != nil
,但我以爲 and
使它們的意圖更加清晰。
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: Int
if 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
。
這是現有 Swift 概念的另外一個擴展,zip
能夠組合多個可選值,它們一塊兒解包成功或解包失敗。在上面的代碼片斷中,我提供了 zip2
與 zip3
函數,但你也能夠命名爲 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
才能徹底掌握它。
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() })
複製代碼
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
}
}
複製代碼
這個方法相似於一個守護者同樣,只有可選值知足 predicate
條件時才進行解包。好比說,咱們但願全部的老用戶都升級爲高級帳戶,以便與咱們保持更長久的聯繫。
// 僅會影響 id < 1000 的用戶
// 正常寫法
if let aUser = user, user.id < 1000 { aUser.upgradeToPremium() }
// 使用 `filter`
user.filter({ $0.id < 1000 })?.upgradeToPremium()
複製代碼
在這裏,user.filter
使用起來更加天然。此外,它的實現相似於 Swift 集合中的功能。
這是我最喜歡的功能之一。這是我從 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
}
複製代碼
至此咱們已經實現了一系列很是有用的可選項擴展。我將會給出個綜合示例,以便更好的瞭解如何組合使用這些擴展。首先,咱們須要先說明一下這個示例,原諒我使用這個不太恰當的例子:
假如你是爲 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 = String
typealias DatabaseSoftware = String
protocol 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") })
}
}
}
複製代碼
若是咱們對比下,至少會有兩點映入眼簾:
在組合使用可選擴展時,我故意形成一種過載狀態。其中的一部分使用很恰當,可是另外一部分卻不那麼合適。然而,使用擴展的關鍵不在於過分依賴(正如我上面作的那樣),而在於這些擴展是否使語義更加清晰明瞭。比較上面的兩個實現方式, 在第二個實現中,考慮下是使用 Swift 自己提供的功能好仍是使用可選擴展更佳。
這就是本文的所有內容,感謝閱讀!
本文由 SwiftGG 翻譯組翻譯,已經得到做者翻譯受權,最新文章請訪問 swift.gg。