可有可無的 Optional 是 Swift 裡一個非常特別的角色。你看它號稱可有可無,我們卻還要認識他,就知道他多特別了。有了它,不管何種型別的變數或常數,都可以沒有任何內容,也就是無值的狀態。至於這有什麼好呢? 這故事得回到很久很久以前,從沒有 Optional 的程式世界說起。
Optional 發明的緣由
有了變數和常數,我們可以輕易儲存任何型別的資料。但在某些特別的情境,我們希望變數和常數能處於一個沒有內容的狀態。比方我們開發一個處理學生考試分數的程式,宣告變數 peterGrade
儲存班上最聰明學生 Peter 考試的分數。
但是 Peter 很有正義感,很可能為了英雄救美或拯救世界而缺考呀。這時候 peterGrade
要儲存什麼樣的內容才能表示缺考呢 ? 應該不能是 0,因為學生有可能考 0 分,0 分和缺考是不一樣的。我們希望可以在程式裡判斷是否缺考,給缺考的學生補考的機會。
也許 -1 是個不錯的解答,畢竟不可能有學生考 -1 分。將 peterGrade
的內容預設為 -1
,待 Peter 考完試後,再改為他實際的考試分數。如此我們即可透過比對分數是否等於 -1,判斷學生是否考過試。
但這種做法其實危險到了極點,它有著以下兩個可怕的缺點:
- 容易誤解。
-1 本身是個真實存在的數字,我們以它表達還沒有分數的狀態其實不太合理。閱讀程式碼時,它不只無法直覺傳達出 Peter 還沒有考試,甚至有可能讓人誤會,以為老師跟 Peter 有仇,才會給他比 0 分還不如的 -1 分。 -
修改容易出問題。
當我們想要採用另一個數字表示沒有分數的狀態,比方 -2,此時必須十分仔細地檢查程式碼,確保所有比對peterGrade
的地方都換成跟 -2 比較,只要稍有遺漏即會產生錯誤的結果。
Optional 變數(常數)的宣告
剛剛提到的這些問題,出自於變數(常數)無法表達沒有內容的狀態。但當 optional 橫空出世後,這些問題都迎刃而解了! Optional 的中文意思是「非必須的,可選擇的」,而在 Swift 程式裡,所有我們宣告的變數(常數),不管型別是什麼,都可以幫它添加 optional 的特性,使其成為一個可以有值,也可以無值的特別變數(常數)。
一個 optional 的變數(常數),可以被設定為 nil
,表示它處於無值的狀態,不管它的型別是什麼,例如以下例子:
在宣告變數(常數)時,於型別的後頭接上問號,即表示它是一個 optional 變數(常數)。在剛剛的例子裡,girlFriend
可以儲存女朋友的名字,也可被設為 nil
,處於單身的可憐狀態。
用問號來表示 optional 是個十分直覺的設計,因為問號的意思正代表了有或沒有,完全說明了它可能有值,也可能無值。在宣告 optional 時,切記問號需緊貼著型別。它們之間的感情可是如膠如漆,連一個空白都容不下!
在剛剛的例子裡,雖然我們尚未將任何值指派給 girlFriend
,但它早已貼心地被自動初始為 nil
。也就是它的效果和以下額外指派 nil
的程式碼是一樣的。
此自動初始化變數的貼心服務,可是只有 optional 變數獨享。若是非 optional,也就是沒有加上問號的變數,它可是無福享有自動初始的福利。如以下例子,當我們宣告 age
後,若是沒有另外指定數值,此時它將不會被自動初始。而在 Swift 的世界裡,還沒初始的變數是不能使用的。因此當我們將 age
乘上10
時,馬上顯示錯誤,因為只有上帝才知道無值的 age
乘以 10
的結果是什麼。
只有 optional 變數(常數)有特權,可以被設為nil
。若是希望即使彗星撞地球,變數(常數)依然不會一夕之間變成 nil
,只要記得別將變數(常數)設為 optional 即可。比方以下例子,若是我們誤將非 optional 的 age1
設為 nil
,紅色錯誤將立即出現警告我們。
有了 optional 表達無值的狀態後,請記得只有 nil
才代表沒有內容。數字 0 和空字串 “” 其實都還是有內容,跟 nil
是不一樣的。
學會 optional 後,未來我們在宣告變數(常數)時,應當在宣告時就想清楚,思考它是否可能無值,是否需要設為 optional。比方 girlFriend
應該是 optional,因為不一定有女朋友,而 mother
應是非 optional,因為一定會有媽媽,否則你根本不會存在這個世界上。養成將可能無值的設為 optional,永遠不該無值的設為非 optional 的良好習慣,即可讓我們的程式變得更安全,降低製造出難以捉摸問題的機率。
Optional 變數(常數)的設定和讀取
在設定 optional 變數(常數)時,其實跟設定非 optional 沒有任何差別,比方以下例子將 girlFriend
設為彼得潘命中注定的女朋友,Wendy 小姐 !
然而 optional 的讀取卻有些許不同。比方以下例子,為了讓女朋友開心,我們特別利用字串相加,加上真心不騙的形容詞「可愛的」。此時卻冒出了錯誤,告訴我們 value of optional type String?’ not unwrapped; did you mean to use ‘!’ or ‘?’?。
難道是 Swift 看出我們說謊,只是哄 Wendy 開心的甜言蜜語嗎 ? 當然不是,學 Swift 的人都是很真心的。
錯誤的原因出自 optional 變數(常數)儲存的內容是被包裝起來的,我們需要解開包裝,才能讀取它的內容。可以想像 optional 變數(常數)的內容被裝在一個精美包裝的盒子裡。當它沒有內容時,即表示盒子是空的。當我們存了字串 「Wendy」 到 girlFriend
裡時,盒子裡將裝著彼得潘心愛的「Wendy」。
既然被盒子裝著,當盒子沒有被打開時,我們當然看不到裡面的內容。(除非你有特異功能) 所以我們必須解開包裝,打開盒子,才能看到它儲存的內容。
要怎麼解開包裝呢? 很簡單。只要請出問號的好兄弟,驚嘆號 !
幫忙即可。當我們在 girlFriend
後接上 !
,即可取出其中儲存的字串 「Wendy」。
利用驚嘆號解開包裝讀取內容的方法稱為 force-unwrap ,也就是強迫解包裝。所有事情只要是強迫的,都有它的風險。當 optional 變數(常數)沒有任何東西,也就是被設為 nil
時,若是我們魯莽地加上驚嘆號,想打開潘朵拉的盒子,取得裡頭的東西,將發生可怕的慘劇 !
也許某一天 Wendy 突然移情別戀,愛上了虎克船長,所以彼得潘成為黃金單身漢,沒有女朋友,girlFriend
被設為 nil
。此時當我們用 ! 讀取 girlFriend
的內容時,將造成程式閃退。下方的 Debug Area 區塊解釋了閃退的原因,因為unexpectedly found nil while unwrapping an Optional value (在解開包裝時發現nil ), Swift 不允許我們用 ! 讀取沒有內容的 optional 變數(常數)。
你可以用現實世界的例子來想像,對方沒有女朋友已經很傷心了,你還硬要逼問他女朋友叫什麼名字,此時他當然會狠狠地一拳打在你臉上,讓你血流成河,就像程式裡出現的紅色錯誤一樣。類似的例子很多,所以請記得,當對方沒錢時,就別再追問他存款有多少,當對方沒朋友時,就別再追問他 FB 有幾個朋友。請別在對方傷口灑鹽了 !
從剛剛的例子,我們也明白了Swift 的用心良苦。因為 ! 暗示了危險,提醒我們用 ! 讀取 optional 變數(常數)要小心,當讀不到內容時程式會閃退。
檢查Optional 變數(常數)是否有值
想預防因為從無值的 optional 變數(常數)強迫取值造成的閃退,其實很簡單。只要在取值前先用 if
判斷它是否不等於 nil
。若是不等於 nil
,即表示目前有值,才可放心地取值。
判斷是否有值,有值的話接著取值的 Optional Binding
利用 if
先判斷 optional 變數(常數)是否有值,若有值再用驚嘆號取值的做法可說是萬無一失,程式終於不會再閃退了。但這樣子的做法其實有點麻煩,因為要分為以下兩個步驟:
- 判斷 optional 變數(常數)是否有值。
- 若 optional 變數(常數)有值,再安心地用驚嘆號取值。
多一事不如少一事,因此 Swift 發明了 optional binding,將兩個步驟濃縮為一個步驟。optional binding 的語法如下,以 if let
+ 常數名稱或 if var
+ 變數名稱開頭,然後再接著 =
和 optional 變數(常數)。
也許初次相見,你會覺得它有點難懂,就像彼得潘初次看到夢中情人 Wendy 一樣。接下來讓我們看看實際的例子,以 if let
說明,你將發現它其實很平易近人的。
說明:
- 第三行的 optional binding 語法將判斷 = 右邊的 optional 變數
girlFriend
是否有值。若是girlFriend
有值,它將從girlFriend
中讀取字串,儲存於新發明的常數girlFriendName
。(因此girlFriendName
的型別為String
,不是 optional) 此時if
判斷式將成立,所以它會執行 { } 裡的程式碼, 在 { } 裡我們可以直接存取girlFriendName
,不用再透過驚嘆號取值。 -
若是不幸分手,
girlFriend
為nil
,if
判斷不成立,程式將跳到else
執行,彼得潘只好夜夜到錢櫃唱歌消愁,和一群男人唱著胡彥斌的男人KTV。
透過 optional binding,現在我們只要一行程式碼即可判斷 optional 變數(常數)是否有值,並將它儲存於某個常數後讀取內容,不用再加 !
取值。
Optional binding 什麼都好,偏偏有個小小的缺點。為了使用它,我們每次都得想個新的常數名稱,接在 if let
之後,就像剛剛為了儲存女朋友的名字, girlFriend
的內容,另外創建一個新的常數 girlFriendName
。
想名稱其實是件挺累的事情,不管是為常數取名還是為小寶寶取名都一樣。Swift 為了怕我們想名字想到頭髮都白了,特別網開一面,允許我們使用 optional binding 時,將 if let
後的常數跟 = 右邊的 optional 變數(常數)取一樣名稱,例如以下例子:
也許有人會尖叫,大喊一樣名稱怎麼可以!到時候 { } 裡的 girlFriend 到底指的是誰呢 ? 這時候當然有人要被犧牲呀,Swift 早就想好了, { } 裡使用的是 if let 後的 girlFriend
,因為它就是我們要的,它儲存著我們想從 optional 變數讀取的內容。
剛剛我們一直以 if let
舉例,另一種寫法 if var
其實也是一樣的概念,兩者的差別在於 let
產生常數,var
產生變數。由於大部份的時候我們只是想讀取內容,而不是修改,所以採用 if let
居多。
Optional binding 人真得很好,幫我們精簡步驟,讓我們開發速度更快。但彼得潘在這裡還是要揭穿它的真面目。其實它是個花心大蘿蔔,腳踏兩條船,它除了可以跟 if
結合,也能搭配 while
。比方 while let girlFriendName = girlFriend
。不過大部分的時候,它都還是很專情地愛著 if
啦,主要出現於搭配 if
的情境。
自動取值的Implicitly Unwrapped Optional
若是變數(常數)可能無值,但是它大多數時候有值,或是一旦被設定內容後,就會保持在有值的狀態,不會再變成 nil
,則可利用 Implicitly Unwrapped Optional,自動解包裝的技術,方便我們直接取值,而不用麻煩地補上驚嘆號取值。
怎麼讓 optional 變數(常數)擁有自動取值的功能呢? 很簡單,只要在宣告時,將原來的問號換成取值的驚嘆號即可,如此 optional 變數(常數)即會時時刻刻記得取值這件事。
任何事情都是有代價的,自動取值為我們帶來很大的方便,但它也有潛藏的危險。若是 optional 變數(常數)為 nil
,一樣的,取值將帶來可怕的閃退。由於它是自動取值,不用再額外添加驚嘆號,因此只要我們在程式裡使用它做運算,即會馬上讀取,造成致命的閃退後果。
因此使用驚嘆號宣告變數(常數)時,其實得更加小心,因為不用加驚嘆號即會自動取值。最好能保證它一旦有值後,就不會再變成 nil
狀態,才不會產生意外的閃退。
若是想要更安全,也可於每次取值前先利用 if
做判斷,避免在 nil
情況下自動取值造成閃退。此外由於它會自動取值,所以我們不需要 optional binding,可直接讀取它的內容做運算。
無值怎麼辦? 變出預設值的雙重問號
Optional 給予我們無值 nil
的彈性。但有些時候,我們仍希望在無值時有個預設值,例如以下例子,當沒有女朋友,girlFriend
為 nil
時,一律回覆女朋友是 Angelababy:
有感於我們的需求,Swift 特別發明了即使 nil
也能變出預設值的雙重問號。在雙重問號的左邊接 optional 變數(常數),右邊接預設值,當 optional 不為 nil
時,即解開它的包裝取值。而當 optional 為 nil
時,則回傳預設值。
如圖所示,當 girlFriend
有值時,girlFriendName
順利地設為解開包裝後的字串 「Wendy」。而當 girlFriend
為 nil
時,girlFriendName
則被設為預設值,彼得潘心目中的女神 「Angelababy」! 值得注意的,雙重問號有點害羞,不喜歡和左右兩邊的鄰居靠得太近,所以請記得保留至少一個空白的間距,以免造成紅色錯誤。
總結
透過 Optional,我們的程式將變得更安全更可靠。所有我們宣告的變數(常數),不管型別是什麼,都可以幫它添加 optional 的特性,使其成為一個可以有值,也可以無值的特別變數(常數)。但在讀取它的內容時也請記得要額外添加 !。關於 optional 或 iOS App 開發的相關技術,大家若有任何問題,可在這裡留言。也歡迎隨時聯絡 彼得潘。當彼得潘回答大家的問題時,其實也在找答案的過程中精進學習,增長了自己的功力,和大家交了朋友,獲得再多錢也買不到的回報和收獲。