問題及解決方法
哨崗值 - 可選值產(chǎn)生的背景
在編程世界中有一種非常通用的模式问芬,那就是某個操作是否要返回一個有效值。很多情況下贮配,一些操作由于各種原因沒有正常的返回期望的值囊卜,而是返回了一個“魔法”數(shù)來表示沒有返回真實(shí)的值。這樣的值被稱為“哨崗值”越妈。而由于我們忘記檢查哨崗值而導(dǎo)致程序出錯季俩,此外,這些哨崗值的檢查非常麻煩具有很多不確定性梅掠,可能我們需要查看文檔酌住,可能文檔也是錯誤的。
通過枚舉解決魔法數(shù)的問題
大多數(shù)語言支持某種類型的枚舉瓤檐,Swift更進(jìn)一步赂韵,他的枚舉中包含‘關(guān)聯(lián)值’的概念。也就是說枚舉可以在它們的值中包含另外的關(guān)聯(lián)的值挠蛉,如下:
enum Optional<T> {
case None
case Some(T)
}
獲取關(guān)聯(lián)值的唯一方法是使用 switch 或者 if case 語句祭示。和哨崗值不同,除非你顯式地檢查并解包谴古,你是不可能意外地使用到一個 Optional 中的值的质涛。
因此,Swift 中與 find 等效的方法 indexOf 所返回的不是一個索引值掰担,而是一個 Optional<Index>汇陆。它是通過協(xié)議擴(kuò)展實(shí)現(xiàn)的:
extension CollectionType where Generator.Element: Equatable {
func indexOf(element: Generator.Element) -> Optional<Index> {
for idx in self.indices where self[idx] == element {
return .Some(idx)
}
// 沒有找到,返回 .None
return .None
}
}
可選值遵守 NilLiteralConvertible 協(xié)議带饱,因此你可以用 nil 來替代.None毡代;像上面 idx 這樣的非可選值將在需要的時候自動“升級”為可選值,這樣你就可以直接寫 return idx勺疼,而不用 return .Some(idx)教寂。
現(xiàn)在,用戶就不會錯誤地使用一個無效的值了:
var array = ["one", "two", "three"]
let idx = array.indexOf("four")
// 編譯錯誤:removeIndex takes an Int, not an Optional<Int>
array.removeAtIndex(idx)”
如果你得到的可選值不是 .None执庐,現(xiàn)在想要取出可選值中的實(shí)際的索引的話酪耕,你必須對其進(jìn)行“解包”:
switch array.indexOf("four") {
case .Some(let idx):
array.removeAtIndex(idx)
case .None:
break // 什么都不做
}
Swift 2.0 中引入了使用 ? 作為在 switch 中對 Some 進(jìn)行匹配的模式后綴的語法,另外轨淌,你還可以使用 nil 字面量來匹配 None:
switch array.indexOf("four") {
case let idx?:
array.removeAtIndex(idx)
case nil:
break // 什么都不做
}
可選值概覽
if let
使用 if let 來進(jìn)行可選綁定 (optional binding) 要比上面使用 switch 語句要稍好一些:
if let idx = array.indexOf("four") {
array.removeAtIndex(idx)
}
if let idx = array.indexOf("four") where idx != array.startIndex {
array.removeAtIndex(idx)
}
你也可以在同一個 if 語句中綁定多個值迂烁。更贊的是,后面的綁定值可以基于之前的成功解包的值來進(jìn)行操作递鹉。這在你想要多次調(diào)用一些返回可選值的函數(shù)時會特別有用盟步。
let urlString = "http://www.objc.io/logo.png" where url.pathExtension == "png"
if let url = NSURL(string: urlString),
data = NSData(contentsOfURL: url),
image = UIImage(data: data)
{
let view = UIImageView(image: image)
XCPlaygroundPage.currentPage.liveView = view
}
如果你需要在指定 if let 綁定之前執(zhí)行某個檢查的話,可以為 if 提供一個前置的條件躏结。如下:使用 NSScanner 來進(jìn)行掃描却盘,它將返回一個代表是否掃描到某個值的布爾值,在之后,你可以解包得到的結(jié)果谷炸。
let stringScanner = NSScanner(string: "myUserName123")
var username: NSString?
let alphas = NSCharacterSet.alphanumericCharacterSet()
if stringScanner.scanCharactersFromSet(alphas, intoString: &username),
let name = username
{
print(name)
}
while let
while let 語句和 if let 非常相似北专,它代表一個當(dāng)遇到 nil 時終止的循環(huán)。
標(biāo)準(zhǔn)庫中的 readLine 函數(shù)從標(biāo)準(zhǔn)輸入中讀取一個可選字符串旬陡。當(dāng)?shù)竭_(dá)輸入末尾時拓颓,這個方法將返回 nil。
while let line = readLine() {
print(line)
}
和 if let 一樣描孟,你可以在可選綁定后面添加一個 where 語句驶睦。如果你想在遇到 EOF 或者空行的時候終止循環(huán)的話,只需要加一個判斷空字符串的語句就行了匿醒。要注意场航,一旦條件為 false,循環(huán)就會停止 (也許你錯誤地認(rèn)為 where 條件會像 filter 那樣工作廉羔,其實(shí)不然)溉痢。
while let line = readLine() where !line.isEmpty {
print(line)
}
let array = [1, 2, 3]
var generator = array.generate()
while let i = generator.next() {
print(i)
}
雙重可選值
一個可選值本身也可以被使用另一個可選值包裝起來,這會導(dǎo)致可選值嵌套在可選值中憋他。假設(shè)你有一個字符串?dāng)?shù)組孩饼,其中的字符串是數(shù)字,你現(xiàn)在想將它們轉(zhuǎn)換為整數(shù)竹挡。最直觀的方式是用一個 map 來進(jìn)行轉(zhuǎn)換:
let stringNumbers = ["1", "2", "3", "foo"]
let maybeInts = stringNumbers.map { Int($0) }
你現(xiàn)在得到了一個元素類型為 Optional<Int> 的數(shù)組镀娶,也就是說,maybeInts 是 [Int?] 類型揪罕。
當(dāng)使用 for 循環(huán)遍歷這個結(jié)果數(shù)組時梯码,顯然每個元素都會是可選整數(shù)值,因?yàn)?maybeInts 含有的就是這樣的值:
for maybeInt in maybeInts {
// maybeInt 是一個 Int? 值
// 得到三個整數(shù)值和一個 `nil`
}
for...in 是 while 循環(huán)加上一個生成器的簡寫方式好啰,生成器的 next 函數(shù)返回的其實(shí)是一個 Optional<Optional<Int>> 值轩娶,或者說是一個 Int??。
當(dāng)循環(huán)到達(dá)最后一個值坎怪,也就是從 “foo” 轉(zhuǎn)換而來的 nil 時罢坝,從 next 返回的其實(shí)是一個非 nil 的值廓握,這個值是 .Some(nil)搅窿。while let 將這個值解包,并將解包結(jié)果 (也就是 nil) 綁定到 maybeInt 上隙券。解決方法:使用for case語法:
for case let i? in maybeInts {
// i 將是 Int 值男应,而不是 Int?
// 1, 2, 和 3
}
// 或者只對 nil 值進(jìn)行循環(huán)
for case nil in maybeInts {
// 將對每個 nil 執(zhí)行一次
}
if var and while var
除了 let 以外,你還可以使用 var 來搭配 if 和 while:
if var i = Int(s) {
i += 1
print(i) // 打印 2
}
可選值是值類型娱仔,解包一個可選值做的事情是將它里面的值提取出來沐飘。所以使用 if var 這個變形和在函數(shù)參數(shù)上使用 var 類似,它只是獲取一個能在作用域內(nèi)使用的副本的簡寫,而并不會改變原來的值耐朴。
解包后可選值的作用域
有時候只能在 if 塊的內(nèi)部訪問被解包的變量確實(shí)讓人有點(diǎn)不爽借卧,但是這其實(shí)和其他一些做法并無不同。
不過如果你從函數(shù)中提早退出的話筛峭,情況就完全不同了铐刘。有時候你可能會這么寫:
func doStuff(withArray a: [Int]) {
if a.isEmpty { return }
// 現(xiàn)在可以安全地使用 a[0]
}
提早退出有助于避免惱人的 if 嵌套,你也不再需要在函數(shù)后面的部分再次重復(fù)地進(jìn)行判斷影晓。此外還可以利用swift的延時初始化來實(shí)現(xiàn):
func doStuffWithFileExtension(fileName: String) {
let period: String.Index
if let idx = fileName.characters.indexOf(".") {
period = idx
} else {
return
}
let extensionRange = period.successor()..<fileName.endIndex
let fileExtension = fileName[extensionRange]
print(fileExtension)
}
雖然避免了if嵌套镰吵,但是這段代碼看起來很丑。我們在這里真正需要的其實(shí)是一個 if not let 語句挂签,其實(shí)這正是 guard let 所做的事情疤祭。
func doStuffWithFileExtension(fileName: String) {
guard let period = fileName.characters.indexOf(".") else { return }
let extensionRange = period.successor()..<fileName.endIndex
let fileExtension = fileName[extensionRange]
print(fileExtension)
}
在閱讀代碼時,guard 是一個明確的信號饵婆,它暗示我們“只在條件成立的情況下繼續(xù)”勺馆。最后 Swift 編譯器還會檢查你是否確實(shí)在 guard 塊中退出了當(dāng)前作用域,如果沒有的話侨核,你會得到一個編譯錯誤谓传。因?yàn)榭梢缘玫骄幾g器幫助,所以我們建議盡量選擇使用 guard芹关,即便 if 也可以正常工作续挟。
可選鏈
在 Objective-C 中,對 nil 發(fā)消息什么都不會發(fā)生侥衬。Swift 里诗祸,我們可以通過“可選鏈 (optional chaining)”來達(dá)到同樣的效果。
self.delegate?.callback()
如果你的可選值值中確實(shí)有值轴总,那么編譯器能夠保證方法肯定會被實(shí)際調(diào)用直颅。如果沒有值的話,這里的問號對代碼的讀者來說是一個清晰地信號怀樟,表示方法可能會不被調(diào)用功偿。
還有如下情況:
let dictOfArrays = ["nine": [0, 1, 2, 3, 4, 5, 6, 7]]
let sevenOfNine = dictOfArrays["nine"]?[7] ”
let dictOfFuncs: [String: (Int, Int) -> Int] = [
"add": (+),
"subtract": (-)
]
dictOfFuncs["add"]?(1, 1)
也可以使用可選值鏈來進(jìn)行賦值,如果它不是 nil 的話往堡,賦值操作將會成功:
splitViewController?.delegate = myDelegate”
nil 合并運(yùn)算符
很多時候械荷,你會想要解包一個可選值,如果可選值是 nil 時虑灰,就用一個默認(rèn)值來替代它吨瞎。你可以使用 nil 合并運(yùn)算符來完成這件事:
let stringteger = "1"
let i = Int(stringteger) ?? 0
當(dāng)你發(fā)現(xiàn)你在檢查某個語句來確保取值滿足條件的時候,往往意味著使用可選值會是一個更好的選擇穆咐。假設(shè)你要做的不是對空數(shù)組判定颤诀,而是要檢查一個索引值是否在數(shù)組邊界內(nèi):
let i = array.count > 5 ? a[5] : 0
不像 first 和 last字旭,通過索引值從數(shù)組中獲取元素不會返回Optional,不過我們可以對 Array 進(jìn)行擴(kuò)展來包含這個功能:
extension Array {
subscript(safe idx: Int) -> Element? {
return idx < endIndex ? self[idx] : nil
}
}
現(xiàn)在你就可以這樣寫:
let i = array[safe: 5] ?? 0
合并操作也能夠進(jìn)行鏈接 — 如果你有多個可能的可選值崖叫,并且想要選擇第一個非 nil 的值遗淳,你可以將它們按順序合并:
let i: Int? = nil
let j: Int? = nil
let k: Int? = 42
let n = i ?? j ?? k ?? 0
可選值 map
在之前你看到過這個例子:
func doStuffWithFileExtension(fileName: String) {
guard let period = fileName.characters.indexOf(".") else { return }
let extensionRange = period.successor()..<fileName.endIndex
let fileExtension = fileName[extensionRange]
print(fileExtension)
}
我們可以稍作改變,現(xiàn)在不在 else 塊中從函數(shù)返回心傀,而是將 fileExtension 聲明為可選值洲脂,并且在 else 中將它設(shè)置為 nil:
func doStuffWithFileExtension(fileName: String) {
let fileExtension: String?
if let idx = fileName.characters.indexOf(".") {
let extensionRange = idx.successor()..<fileName.endIndex
fileExtension = fileName[extensionRange]
} else {
fileExtension = nil
}
print(fileExtension ?? "No extension")
}
Swift 中的可選值里專門有一個方法來處理這種情況,它叫做 map剧包。這個方法接受一個閉包恐锦,如果可選值有內(nèi)容,則調(diào)用這個閉包對其進(jìn)行轉(zhuǎn)換疆液。上面的函數(shù)用 map 可以重寫成:
func doStuffWithFileExtension(fileName: String) {
let fileExtension: String? = fileName.characters.indexOf(".").map { idx in
let extensionRange = idx.successor()..<fileName.endIndex
return fileName[extensionRange]
}
print(fileExtension ?? "No extension")
}
顯然一铅,這個 map 和數(shù)組以及其他序列里的 map 方法非常類似。但是與序列中操作一系列值所不同的是堕油,可選值的 map 方法只會操作一個值潘飘,那就是該可選值中的那個可能的值。你可以把可選值當(dāng)作一個包含零個或者一個值的集合掉缺,這樣 map 要么在零值的情況下不做處理卜录,要么在有值的時候會對其進(jìn)行轉(zhuǎn)換。
當(dāng)你想要的就是一個可選值結(jié)果時眶明,可選值 map 就非常有用艰毒。設(shè)想你想要為數(shù)組實(shí)現(xiàn)一個變種的 reduce 方法,這個方法不接受初始值搜囱,而是直接使用數(shù)組中的首個元素作為初始值.
[1, 2, 3, 4].reduce(+)
因?yàn)閿?shù)組可能會是空的丑瞧,這種情況下沒有初始值,結(jié)果只能是 nil蜀肘,所以這個結(jié)果應(yīng)當(dāng)是一個可選值绊汹。你可能會這樣來實(shí)現(xiàn)它:
extension Array {
func reduce(combine: (Element, Element) -> Element) -> Element? {
// 如果數(shù)組為空,self.first 將是 nil
guard let fst = first else { return nil }
return self.dropFirst().reduce(fst, combine: combine)
}
}
因?yàn)榭蛇x值為 nil 時扮宠,可選值的 map 也會返回 nil西乖,所以我們可以使用不包含 guard 的單 return 形式來重寫 reduce:
extension Array {
func reduce(combine: (Element, Element) -> Element) -> Element? {
return first.map {
self.dropFirst().reduce($0, combine: combine)
}
}
}
鑒于可選值 map 與集合的 map 的相似性,可選值 map 的實(shí)現(xiàn)和集合 map 也很類似:
extension Optional {
func map<U>(transform: Wrapped -> U) -> U? {
if let value = self {
return transform(value)
}
return nil
}
}
可選值 flatMap
如果你的序列中包含可選值坛增,可能你會只對那些非 nil 值感興趣获雕。實(shí)際上,你可以忽略掉那些 nil 值轿偎。
設(shè)想你需要處理一個字符串?dāng)?shù)組中的數(shù)字典鸡。在有可選值模式匹配時被廓,用 for 循環(huán)可以很簡單地就實(shí)現(xiàn):
let numbers = ["1", "2", "3", "foo"]
var sum = 0
for case let i? in numbers.map({ Int($0) }) {
sum += i
}
你可能也會想用 ?? 來把 nil 替換成 0:
numbers.map { Int($0) }.reduce(0) { $0 + ($1 ?? 0) }
實(shí)際上坏晦,你想要的版本應(yīng)該是一個可以將那些 nil 過濾出去并將非 nil 值進(jìn)行解包的 map。標(biāo)準(zhǔn)庫中序列的 flatMap 正是你想要的:
numbers.flatMap { Int($0) }.reduce(0, combine: +)
可選值判等和比較
在判等時你不需要關(guān)心一個值是不是 nil,你只需要檢查它是否包含某個 (非 nil 的) 特定值即可:
if regex.characters.first == "^" {
// 只匹配字符串開頭
}
上面的代碼之所以能工作主要基于兩點(diǎn)昆婿。首先球碉,== 有一個接受兩個可選值的版本,它的實(shí)現(xiàn)類似這樣:
func ==<T: Equatable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case (nil, nil): return true
case let (x?, y?): return x == y
case (_?, nil), (nil, _?): return false
}
}
強(qiáng)制解包的時機(jī)
上面提到的例子都用了很利索的方式來解包可選值仓蛆,什么時候你應(yīng)該用感嘆號 (!) 這個強(qiáng)制解包運(yùn)算符呢睁冬?
當(dāng)你能確定你的某個值不可能是 nil 時可以使用嘆號,你應(yīng)當(dāng)會希望如果它不巧意外地是 nil 的話看疙,這句程序直接掛掉豆拨。
func flatten<S: SequenceType, T where S.Generator.Element == T?>(source: S) -> [T]{
return Array(source.lazy.filter { $0 != nil }.map { $0! })
}
這里,因?yàn)樵?filter 的時候已經(jīng)把所有 nil 元素過濾出去了能庆,所以 map 的時候沒有任何可能會出現(xiàn) $0! 碰到 nil 值的情況施禾。
不過使用強(qiáng)制解包還是很罕見的。
第二個例子搁胆,下面這段代碼會根據(jù)特定的條件來從字典中找到值滿足這個條件的對應(yīng)的所有的鍵:
let ages = [
"Tim": 53, "Angela": 54, "Craig": 44,
"Jony": 47, "Chris": 37, "Michael": 34,
]
let people = ages
.keys
.filter { name in ages[name]! < 50 }
.sort()
這里使用 ! 非常安全 — 因?yàn)樗械逆I都是來源于字典的弥搞,所以在字典中找不到這個鍵是不可能的。
改進(jìn)強(qiáng)制解包的錯誤信息
其實(shí)渠旁,你可能會留一個注釋來提醒為什么這里要使用強(qiáng)制解包攀例。那為什么不把這個注釋直接作為錯誤信息呢?這里我們加了一個 !! 操作符顾腊,它將強(qiáng)制解包和一個更具有描述性質(zhì)的錯誤信息結(jié)合在一起粤铭,當(dāng)程序意外退出時,這個信息也會被打印出來:
infix operator !! { }
func !! <T>(wrapped: T?, @autoclosure failureText: ()->String) -> T {
if let x = wrapped { return x }
fatalError(failureText())
}
let s = "foo"
let i = Int(s) !! "Expecting integer, got \"\(s)\"”
@autoclosure 注解確保了我們只在需要的時候會執(zhí)行操作符右側(cè)的語句杂靶。
在調(diào)試版本中進(jìn)行斷言
通常承耿,你可能會選擇在調(diào)試版本或者測試版本中進(jìn)行斷言,讓程序崩潰伪煤,但是在最終產(chǎn)品中加袋,你可能會把它替換成像是零或者空數(shù)組這樣的默認(rèn)值。
我們可以實(shí)現(xiàn)一個疑問感嘆號 !? 操作符來代表這個行為抱既。我們將這個操作符定義為對失敗的解包進(jìn)行斷言职烧,并且在斷言不觸發(fā)的發(fā)布版本中將值替換為默認(rèn)值:
infix operator !? { }
func !?<T: IntegerLiteralConvertible>(wrapped: T?, @autoclosure failureText: ()->String) -> T{
assert(wrapped != nil, failureText())
return wrapped ?? 0
}
現(xiàn)在,下面的代碼將在調(diào)試時觸發(fā)斷言防泵,但是在發(fā)布版本中打印 0:
let i = Int(s) !? "Expecting integer, got \"\(s)\""
對于返回 Void 的函數(shù)蚀之,使用可選鏈進(jìn)行調(diào)用時將返回 Void?。利用這一點(diǎn)捷泞,你可以寫一個非泛型的版本來檢測一個可選鏈調(diào)用碰到 nil足删,且并沒有進(jìn)行完操作的情況:
func !?(wrapped: ()?, @autoclosure failureText: ()->String) {
assert(wrapped != nil, failureText)
}
var output: String? = nil
output?.write("something") !? "Wasn't expecting chained nil here”
想要掛起一個操作我們有三種方式。首先锁右,fatalError 將接受一條信息失受,并且無條件地停止操作讶泰。第二種選擇,使用 assert 來檢查條件拂到,當(dāng)條件結(jié)果為 false 時痪署,停止執(zhí)行并輸出信息。在發(fā)布版本中兄旬,assert 會被移除掉狼犯,條件不會被檢測,操作也永遠(yuǎn)不會掛起领铐。第三種方式是使用 precondition悯森,它和 assert 比較類型,但是在發(fā)布版本中它不會被移除绪撵,也就是說呐馆,只要條件被判定為 false,執(zhí)行就會被停止莲兢。
多災(zāi)多難的隱式可選值
隱式可選值是那些不論何時你使用它們的時候就自動強(qiáng)制解包的可選值汹来。別搞錯了,它們依然是可選值改艇,現(xiàn)在你已經(jīng)知道了當(dāng)可選值是 nil 的時候強(qiáng)制解包會造成應(yīng)用崩潰收班,那你到底為什么會要用到隱式可選值呢?實(shí)際上有兩個原因:
- 暫時來說谒兄,你可能還需要到 Objective-C 里去調(diào)用那些沒有檢查返回是否存在的代碼摔桦。
- 因?yàn)橐粋€值只是很短暫地為 nil,在一段時間后承疲,它就再也不會是 nil邻耕。
隱式可選值行為
因?yàn)殡[式可選值會盡可能地隱藏它們的可選值特性,所以它們在行為上也有一些不一樣燕鸽。
func increment(inout x: Int) {
x += 1
}
// 普通的 Int
var i = 1
// 將 i 增加為 2
increment(&i)
// 隱式解包的 Int
var j: Int! = 1
// 錯誤:cannot invoke 'increment' with an argument list of type '(inout Int!)'
increment(&j)
總結(jié)
在處理有可能是 nil 的值的時候兄世,可選值會非常有用。相比于使用像是 NSNotFound 這樣的魔法數(shù)啊研,我們可以用 nil 來代表一個值為空御滩。Swift 中有很多內(nèi)置的特性可以處理可選值,所以你能夠避免進(jìn)行強(qiáng)制解包党远。隱式解包可選值在與遺留代碼協(xié)同工作時會有用削解,但是在有可能的情況下還是應(yīng)該盡可能使用普通的可選值。最后沟娱,如果你需要比單個可選值更多的信息 (比如氛驮,在結(jié)果不存在時你可能需要一個錯誤信息提示),你可以使用拋出錯誤的方法济似。