讀古今文學網 > iOS編程基礎:Swift、Xcode和Cocoa入門指南 > 3.7 內建簡單類型 >

3.7 內建簡單類型

每個變量與每個值都必須有一個類型。不過類型是什麼呢?到目前為止,我已經假設存在一些類型了,如Int與String,不過並沒有正式對其進行介紹。下面是Swift提供的主要的簡單類型,以及適合於這些內建類型的實例方法、全局函數與運算符。(集合類型將會在第4章最後介紹。)

3.7.1  Bool

Bool對像類型(結構體)只有兩個值,真與假(或是與非)。你可以通過字面關鍵字true與false來表示這些值;顯然,一個Bool值要麼為true,要麼為false:


var selected : Bool = false  

在上述代碼中,selected是個Bool變量,並被初始化為false;隨後可以將其設為false或true,但不能是其他值。由於其簡單的真或假狀態,這種Bool變量通常也叫作標識。

Cocoa有很多方法都接收Bool參數或是返回Bool值。比如,當應用啟動時,Cocoa會調用如下聲明的方法:


func application(application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?)
    -> Bool  

你可以在該方法中做任何事情;但通常什麼都不會做,不過必須要返回一個Bool!在實際情況下,該Bool值總是true。該函數最簡單的實現如下所示:


func application(application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?)
    -> Bool {
        return true
}  

Bool在條件判斷中很有用;第5章將會介紹,在說if something時,那麼something就是個條件,它是個Bool值,或是會得到一個Bool值的表達式。比如,在使用相等比較運算符==來比較兩個值時,結果就是個Bool;如果兩個值相等,那麼結果就為true,否則為false:


if meaningOfLife == 42 { // ...  

(稍後在談及如Int和String等可以進行比較的類型時,我們還會繼續介紹相等比較。)

在準備判斷條件時,有時提前將Bool值存儲到變量中會增強可讀性:


let comp = self.traitCollection.horizontalSizeClass == .Compact
if comp { // ...  

注意到在使用這種方式時,我們直接將Bool變量作為條件。寫成if comp==true這樣是非常愚蠢的做法,也是錯誤的,因為「如果comp為true」,那就沒必要顯式測試它為true還是false;條件表達式本身已經測試過了。

既然Bool可以用作條件,那麼對返回一個Bool值的函數的調用也可以作為條件。如下示例來自於我所編寫的代碼。我聲明了一個返回Bool值的函數,判斷用戶所選擇的底牌是否是謎題的正確答案:


func evaluate(cells:[CardCell]) -> Bool { // ...  

在其他地方可以這樣調用:


if self.evaluate(cellsToTest) { // ...  

與很多計算機語言不同,Swift中沒有任何東西可以隱式轉換為或被當作Bool。比如,在C中,boolean實際上是個數字,0是false。不過在Swift中,除了false,沒有任何東西是false,true亦如此。

類型名Bool源自英國數學家George Boole;布爾代數提供了邏輯運算。Bool值可以應用到這些操作:

非。!一元運算符用在Bool值前面,它會反轉該Bool值。如果ok為true,那麼!ok就為false,反之亦然。

&&

邏輯與。只有兩個操作數都為true才會返回true,否則返回false。如果第1個操作數為false,那麼第2個操作數甚至都不會計算(從而避免可能的副作用)。

||

邏輯或。如果兩個操作數有一個為true就返回true,否則返回false。如果第1個操作數為true,那麼第2個操作數甚至都不會計算(從而避免可能的副作用)。

如果邏輯運算很複雜,那麼對子表達式加上圓括號會有助於釐清運算邏輯與順序。

3.7.2 數字

主要的數字類型是Int與Double,這表示你應該使用這兩種類型。其他數字類型存在的主要目的就是與C和Objective-C API兼容,因為在編寫iOS程序時,Swift需要與它們通信。

1.Int

Int對像類型(結構體)表示介於Int.max與Int.min(包含首尾兩個數字)之間的一個整數。實際的限定值取決於應用運行的平台與架構,因此不要完全依賴它們;在我的測試中,它們分別是263-1與-263(64位)。

表示一個Int最簡單的方式就是將其作為一個數字字面值。在默認情況下,沒有小數點的簡單數字字面值都會被當作Int。可以在數字間使用下劃線,這有助於增強長數字的可讀性。前導的0也是合法的,這有助於填補與對齊代碼中的值。

可以通過二進制、八進制與十六進制來表示Int字面值。要想做到這一點,請分別在數字前加上0b、0o或0x。比如,0x10表示十進制16。

2.Double

Double對像類型(結構體)表示一個精度大約為小數點後15位的浮點數(64位存儲)。

表示一個Double值最簡單的方式就是將其作為一個數字字面值。在默認情況下,包含小數點的任何數字字面值都會被當作Double。可以在數字間使用下劃線與前導0。

Double字面值不能以小數點開頭!如果值介於0到1之間,那麼請以前導0作為字面值的開始。(強調這一點的原因在於這與C和Objective-C有著明顯的差別。)

可以通過科學計數法表示Double字面值。字母e後面的內容就是10的指數。如果小數部分為0,那就可以省略小數點。比如,3e2就表示3乘以102(300)。

還可以通過十六進製表示Double字面值。要想做到這一點,請以0x作為字面值的開頭。這裡也可以使用乘方(還是可以省略小數點);字母p後面的內容就是2的指數。比如,0x10p2就表示十進制64,因為是16乘以22。

除了其他屬性,Double還有一個靜態屬性Double.infinity和一個實例屬性isZero。

3.強制類型轉換

強制類型轉換指的是將一種數字類型的值轉換為另一種。Swift並沒有提供顯式類型轉換,不過通過實例化來達到相同的目的。要想將一個Int顯式轉換為Double,請在圓括號中使用Int來實例化一個Double。要想將一個Double顯式轉換為Int,請在圓括號中使用Double來實例化一個Int;這麼做會截斷原始值(小數點後的一切都會被丟棄):


let i = 10
let x = Double(i)
print(x) // 10.0, a Double
let y = 3.8
let j = Int(y)
print(j) // 3, an Int  

在將數字值賦給變量或作為參數傳遞給函數時,Swift只會執行字面值的隱式轉換。如下代碼是合法的:


let d : Double = 10  

不過如下代碼是非法的,因為你所賦予的變量(非字面值)是另外一種類型;編譯器會阻止你這麼做:


let i = 10
let d : Double = i // compile error  

解決辦法就是在賦值或傳遞變量時進行顯式轉換:


let i = 10
let d : Double = Double(i)  

使用算術運算合併數字值時也需要遵循該原則。Swift只會執行隱式轉換。常見的情況是對Int與Double執行算術運算;Int會被當作Double:


let x = 10/3.0
print(x) // 3.33333333333333  

不過,如果對不同數字類型的變量執行算術運算,這些變量需要進行顯式轉換,這樣才能確保它們都是相同類型。比如:


let i = 10
let n = 3.0
let x = i / n // compile error; you need to say Double(i)  

這些原則顯然都是Swift嚴格類型的結果;不過,與其他現代計算機語言相比,Swift對待數字值的方式有著很大的不同,可能會讓你叫苦不迭。到目前為止,我所給出的示例都很容易,不過如果算術表達式很長,那麼事情就會變得更加複雜,而且問題還會同為了保持與Cocoa兼容所需的其他數字類型交織在一起,下面就來談談。

4.其他數值類型

如果沒有編寫iOS應用,而是單純使用Swift,那麼你可能只會用到Int與Double來完成所有的算術運算。但遺憾的是,編寫iOS程序需要Cocoa,而Cocoa中還有很多其他的數值類型,Swift也提供了與之匹配的類型。因此,除了Int,還有各種大小的有符號整型(如Int8、Int16、Int32及Int64),以及無符號整型UInt、UInt8、UInt16、UInt32及UInt64。除了Double,還有低精度的Float(32位存儲、大約保留小數點後6或7位精度)以及擴展精度的Float80;在Core Graphics框架中還有CGFloat(其大小可以是Float或Double,這取決於架構的位數)。

在使用C API時還會遇到C數值類型。對於Swift來說,這些類型只是類型別名而已,這意味著它們是其他類型的別名;比如,CDouble(對應於C的double)只是Double的另一個名字,CLong(C中的long)是Int類型等。很多其他的數值類型別名都會出現在各種Cocoa框架中;比如,NSTimeInterval只是Double的類型別名而已。

問題來了。我之前曾說過,不能通過變量賦值、傳遞或組合不同數值類型的值;你只能顯式將這些值轉換為正確的類型才行。不過,現在你面對的是Cocoa中眾多類型的數值!Cocoa傳遞給你的數值很可能既不是Int也不是Double,你可能根本就發現不了,直到編譯器告訴你出現了類型不匹配的情況。接下來,你需要搞清楚到底什麼地方錯了,然後將這些變量轉換為相同的類型。

如下這個典型示例來自於我的應用。我有一個UIImage,將其CGImage抽取出來,現在想要通過CGSize來表示該CGImage的大小:


let mars = UIImage(named:"Mars")!
let marsCG = mars.CGImage
let szCG = CGSizeMake( // compile error
    CGImageGetWidth(marsCG),
    CGImageGetHeight(marsCG)
)  

問題在於CGImageGetWidth與CGImageGetHeight返回的是Int,而CGSizeMake接收的卻是CGFloat。這並非C或Objective-C的問題,因為它們可以實現從前者到後者的隱式類型轉換。問題在於Swift,你只能執行顯式類型轉換:


var szCG = CGSizeMake(
    CGFloat(CGImageGetWidth(marsCG)),
    CGFloat(CGImageGetHeight(marsCG))
)  

下面是另一個實際的例子。界面中的滑塊是個UISlider,其minimumValue與maximum-Value都是Float。在如下代碼中,s是個UISlider,g是個UIGestureRecognizer,我們要通過手勢識別器將滑塊移動到用戶輕拍的位置處:


let pt = g.locationInView(s)
let percentage = pt.x / s.bounds.size.width
let delta = percentage * (s.maximumValue - s.minimumValue) // compile error  

上述代碼無法編譯通過。pt是個CGPoint,因此pt.x是個CGFloat。幸好,s.bounds.size.width也是個CGFloat,因此第2行代碼可以編譯通過;現在的percentage被推斷為是個CGFloat。不過在第3行,percentage與s.maximumValue和s.minimumValue一同參與運算,後兩者是Float,並非CGFloat。必須要進行顯式類型轉換:


let delta = Float(percentage) * (s.maximumValue - s.minimumValue)  

圖3-1:快速幫助會顯示出變量的類型

唯一的好消息是,如果大部分代碼都能編譯通過,那麼Xcode的快速幫助特性會告訴你Swift推斷出某個變量的類型到底是什麼(如圖3-1所示)。這可以幫助你定位關於數值類型的問題。

有時,你需要賦值或傳遞一種整型類型,但目標需要的卻是另一種整型類型,而你也不知道到底需要哪一種整型類型,這時可以通過調用numericCast讓Swift進行動態類型轉換。比如,如果i與j是之前聲明的不同整型類型的變量,那麼i=numericCast(j)就會將j強制轉換為i的整型類型。

5.算術運算

Swift的算術運算符與你想的一樣;它們與其他計算機語言和真正的算術運算非常類似:

+

加運算符。將第2個操作數加到第1個並返回結果。

-

減運算符。從第1個操作數中減掉第2個並返回結果。一元減運算符用作操作數的前綴,看起來與它一樣,但返回的卻是操作數的相反數(事實上,還有個一元加運算符,它原樣返回操作數)。

*

乘運算符。將第1個操作數與第2個相乘並返回結果。

/

除運算符。將第1個操作數除以第2個並返回結果。

與C一樣,兩個Int相除得到的還是Int;小數部分均會丟棄掉。10/3的結果為3,而不是3又1/3。

%

餘數運算符。將第1個操作數除以第2個並返回餘數。如果第1個操作數是負數,那麼結果就是負數;如果第2個操作數是負數,那麼結果為正數。浮點操作數是合法的。

整型類型可以看作二進制位,因此可以進行二進制位運算:

&

按位與。如果兩個操作數的同一位均為1,那麼結果就為1。

|

按位或。如果兩個操作數的同一位均為0,那麼結果就為0。

^

按位異或。如果兩個操作數的同一位不同,那麼結果就為1。

~

按位取反。它用在單個操作數之前,對每一位取反並返回結果。

<<

左移。將第1個操作數向左移動第2個操作數所指定的位數。

>>

右移。將第1個操作數向右移動第2個操作數所指定的位數。

從技術上來說,如果整型是無符號的,那麼位移運算符會執行邏輯位移;如果整型是有符號的,那麼它會執行算術位移。

整型上溢或下溢(比如,將兩個Int相加,導致結果超出Int.max)是個運行時錯誤(應用會崩潰)。對於簡單的情況來說,編譯器會阻止你這麼做,但是你可以輕鬆繞過編譯器的檢查:


let i = Int.max - 2
let j = i + 12/2 // crash  

在某些情況下,你希望強制這種操作能夠成功,因此需要提供特殊的上溢/下溢方法。這些方法會返回一個元組;雖然還沒有介紹過元組,但是我還是打算展示這樣一個示例:


let i = Int.max - 2
let (j, over) = Int.addWithOverflow(i,12/2)  

現在,j值為Int.min+3(因為其值已經由原來的對Int.max的包裝變成了對Int.min的包裝),over值為true(用於報告溢出情況)。

如果你對是否存在上溢/下溢的情況不在乎,那麼可以通過特殊的算術運算符來消除錯誤:&+、&-和&*。

你常常會將現有變量值與另一個值合併起來,然後將結果存儲到相同的變量中。請記住,為了做到這一點,你需要將變量聲明為var:


var i = 1
i = i + 7  

作為一種簡便寫法,你可以通過一個運算符一步完成算術運算與賦值:


var i = 1
i += 7  

簡便(復合)賦值算術運算符有+=、-=、*=、/=、%=、&=、|=、^=、~=、<<=和>>=。

我們常常需要將某個數值加1或減1,Swift提供了一元增加與減少運算符++和--。區別在於它們用作前綴還是後綴。如果用作前綴(++i、--i),那麼值就會增加或減少,並存儲到相同的變量中,然後用於外部表達式中;如果用作後綴(i++、i--),那麼變量當前值就會用在外部表達式中,然後值再增加或減少,並存儲到相同變量中。顯然,變量必須要通過var聲明才可以。

運算優先級也是非常直觀的:比如,*的優先級比+要高,因此x+y*z會先執行y*z,然後再將結果與x相加。如果有問題,可以通過圓括號消除歧義;比如,(x+y)*z就會先執行加法操作。

全局函數包含了abs(取絕對值)、max和min:


let i = -7
let j = 6
print(abs(i)) // 7
print(max(i,j)) // 6  

其他數學函數(如取平方根、四捨五入、偽隨機數、三角函數等)都來自於C標準庫,可以正常使用它們,因為你已經導入了UIKit。還得小心數值類型,即便對於字面值來說也沒有隱式轉換。

比如,sqrt接收一個C double,它是個CDouble類型,也是個Double類型。因此,不能寫成sqrt(2),只能寫成sqrt(2.0)。與之類似,arc4random會返回一個UInt32類型。如果n是個Int類型,同時希望得到一個介於0到n-1之間的隨機數,那麼你不能寫成arc4random()%n;只能將調用arc4random的結果強制轉換為Int。

6.比較

數字是通過比較運算符進行比較的,運算符返回一個Bool。比如,表達式i==j用於判斷i與j是否相等;如果i與j是數字,那麼相等就表示數值上的相等。因此,只有i和j是相同的數字,i==j才為true,這與你的期望是完全一致的。

比較運算符有:

==

相等運算符,操作數相等才會返回true。

!=

不等運算符。操作數相等會返回false。

<

小於運算符。如果第1個操作數小於第2個,那麼會返回true。

<=

小於等於運算符。如果第1個操作數小於或等於第2個,那麼會返回true。

>

大於運算符。如果第1個操作數大於第2個,那麼會返回true。

>=

大於等於運算符。如果第1個操作數大於或等於第2個,那麼會返回true。

請記住,基於計算機存儲數字的方式,Double值的相等性比較可能會與你期望的不一致。要想判斷兩個Double是否相等,更可靠的方式是將它們的差值與一個非常小的值進行比較(通常叫作ε)。


let isEqual = abs(x - y) < 0.000001  

3.7.3  String

String對像類型(結構體)表示文本。表示String值最簡單的方式是使用字面值,並由一對雙引號圍起來:


let greeting = "hello"  

Swift字符串是非常現代化的;在底層,它是個Unicode,你可以在字符串字面值中直接包含任意字符。如果不想敲Unicode字符,同時又知道它的代碼,那麼可以使用符號\u{...?},其中花括號之間最多會有8個十六進制數字:


let leftTripleArrow = "\u{21DA}"  

字符串中的反斜槓是轉義字符;它表示「我並不是一個反斜槓,而是告訴你要特別對待下一個字符」。各種不可打印以及容易造成歧義的字符都是轉義字符,最重要的轉義字符有:

\n

UNIX換行符。

\t

製表符。

\"

引號(這裡的轉義是表示它並非字符串字面值的結束)。

\\

反斜槓(因為單獨一個反斜槓是轉義字符)。

Swift最酷的特性之一就是字符串插入。你可以將待輸出的任何值使用print嵌入字符串字面值中作為字符串,即便它本身並非字符串也可以,使用的是轉義圓括號\(...?),比如:


let n = 5
let s = "You have \(n) widgets."  

現在,s表示字符串「You have 5 widgets」。該示例本身沒什麼太大價值,因為我們知道n是什麼,並且可以直接在字符串中輸入5;不過,如果我們不知道n是什麼呢!此外,轉義圓括號中的內容不一定非得是變量的名字;它可以是Swift中任何合法的表達式。如果不知道怎麼用,如下示例會更具價值:


let m = 4
let n = 5
let s = "You have \(m + n) widgets."  

轉義圓括號中不能有雙引號。這令人感到失望,但卻不是什麼障礙;這時只需將其賦給一個變量,然後在圓括號中使用該變量即可。比如,你不能這麼做:


let ud = NSUserDefaults.standardUserDefaults
let s = "You have \(ud.integerForKey("widgets")) widgets." // compile error  

對雙引號轉義也無濟於事,你只能寫成多行,如下代碼所示:


let ud = NSUserDefaults.standardUserDefaults
let n = ud.integerForKey("widgets")
let s = "You have \(n) widgets."  

要想拼接兩個字符串,最簡單的方式是使用+運算符(以及+=賦值簡寫方式):


let s = "hello"
let s2 = " world"
let greeting = s + s2  

這種便捷符號是可以的,因為+運算符已經被重載了:對於操作數是數字以及操作數是字符串的情況,它的行為是不同的,前者執行數字相加,後者執行字符串拼接。第5章將會介紹,所有運算符都可以重載,你可以重載它們以便對自己定義的類型執行恰當的操作。

作為+=的替代,你還可以調用appendContentsOf實例方法:


var s = "hello"
let s2 = " world"
s.appendContentsOf(s2) // or: s += s2  

拼接字符串的另一種方式是使用joinWithSeparator方法。通過一個待拼接的字符串數組調用它(沒錯,我們還沒開始介紹數組呢),並將插入其中的字符串傳遞給該數組:


let s = "hello"
let s2 = "world"
let space = " "
let greeting = [s,s2].joinWithSeparator(space)  

比較運算符也進行了重載,這樣它們就都可以用於String操作數。如果兩個String包含相同的文本,那麼它們就是相等的(==)。如果一個String按照字母表順序位於另一個之前,那麼前一個就小於後一個。

Swift還提供了一些附加的便捷實例方法與屬性。isEmpty會返回一個Bool,表示字符串是否為空字符串("")。hasPrefix與hasSuffix判斷字符串是否以另一個字符串開始或結束;比如,"hello".hasPrefix("he")返回true。uppercaseString與lowercaseString屬性提供了原始字符串的大寫與小寫版本。

可以在String與Int之間進行強制類型轉換。要想創建一個表示Int的字符串,使用字符串插入即可;此外,還可以使用Int作為String初始化器,就好像在數字類型之間進行強制類型轉換一樣:


let i = 7
let s = String(i) // "7"  

字符串還可以通過其他進制來表示Int,提供一個radix:參數來表示進制:


let i = 31
let s = String(i, radix:16) // "1f"  

能夠表示數字的String還可以強制轉換為數字類型;整型類型會接收一個radix:參數來表示基數。不過,這個轉換可能會失敗,因為String可能不是表示指定類型的數字;這樣,結果就不是數字,而是一個包裝了數字的Optional(現在還沒有介紹過Optional,相信我就好;第4章將會介紹可失敗的初始化器):


let s = "31"
let i = Int(s) // Optional(31)
let s2 = "1f"
let i2 = Int(s2, radix:16) // Optional(31)  

實際上,String的強制類型轉換是字符串插值與使用print在控制台打印的基礎。你可以將任何對像轉換為String,方式是讓其遵循如下3個協議之一:Streamable、CustomStringConvertible與CustomDebugStringConvertible。第4章介紹協議時會給出相關的示例。

可以通過characters屬性的count方法獲得String的字符長度:


let s = "hello"
let length = s.characters.count // 5  

為何String沒有提供length屬性呢?這是因為String並沒有一個簡單意義上的長度概念。String是以Unicode編碼序列的形式存在的,不過多個Unicode編碼才能構成一個字符;因此,為了知道一個序列表示多少個字符,我們需要遍歷序列,將其解析為所表示的字符。

你也可以遍歷String的字符。最簡單的方式是使用for...?in結構(參見第5章)。這麼做所得到的是Character對象,稍後將會對其進行深入介紹。


let s = "hello"
for c in s.characters {
    print(c) // print each Character on its own line
}  

在更深的層次上,可以通過utf8與utf16屬性將String分解為UTF-8編碼與UTF-16編碼。


let s = "\u{BF}Qui\u{E9}n?"
for i in s.utf8 {
    print(i) // 194, 191, 81, 117, 105, 195, 169, 110, 63
}
for i in s.utf16 {
    print(i) // 191, 81, 117, 105, 233, 110, 63
}  

還有一個unicodeScalars屬性,它將String的UTF-32編碼集合表示為一個UnicodeScalar結構體。要想從數字編碼構造字符串,請通過數字實例化一個UnicodeScalar並將其append到String上。下面這個輔助函數會將一個兩字母的國家縮寫轉換為其國旗的表情符號:


func flag(country:String) -> String {
    let base : UInt32 = 127397
    var s = ""
    for v in country.unicodeScalars {
        s.append(UnicodeScalar(base + v.value))
    }
    return s
}
// and here's how to use it:
let s = flag("DE")  

奇怪的是Swift並沒有提供更多關於標準字符串操作的方法。比如,如何將一個字符串轉換為大寫,如何判斷某個字符串是否包含了給定的子字符串。大多數現代編程語言都提供了緊湊、方便的方式來做到這一點,但Swift卻不行。原因在於Foundation框架所提供的特性的缺失,在實際開發中你總是會導入它(導入UIKit就會導入Foundation)。Swfit String橋接了Foundation NSString。這意味著在很大程度上,當使用Swift String時,真正使用的卻是Foundation NSString方法。比如:


let s = "hello world"
let s2 = s.capitalizedString // "Hello World"  

capitalizedString屬性來自於Foundation框架,它由Cocoa而非Swift提供。這是個NSString屬性,它是附著在String上的。與之類似,如下代碼展示了如何定位某個字符串中的一個子字符串:


let s = "hello"
let range = s.rangeOfString("ell") // Optional(Range(1..<4))  

現在尚未介紹過Optional和Range(本章後面將會對其進行介紹),不過上述代碼起到了連接Swift與Cocoa的作用:Swift String s變成了一個NSString,NSString rangeOfString方法被調用,返回了一個Foundation NSRange結構體,然後NSRange又被轉換為Swift Range並被包裝為一個Optional。

不過有時,你並不希望進行這種轉換。出於各種各樣的原因,你只想使用Foundation,並接收Foundation NSRange。為了做到這一點,你需要通過as運算符(第4章將會介紹類型轉換)顯式將字符串轉換為NSString:


let s = "hello"
let range = (s as NSString).rangeOfString("ell") // (1,3), an NSRange  

再來看一個示例,該示例也涉及NSRange。假設你想要根據範圍(第2、3、4個字符)從「hello」中獲取到字符串「ell」。Foundation NSString的方法substringWithRange:要求你提供一個範圍,表示一個NSRange。你可以直接通過Foundation函數構造一個NSRange,不過如果這麼做,代碼將無法通過編譯:


let s = "hello"
let ss = s.substringWithRange(NSMakeRange(1,3)) // compile error  

編譯報錯的原因在於Swift已經吸納了NSString的substringWithRange:,它這裡希望你提供一個Swift Range。稍後將會介紹如何做到這一點,不過通過類型轉換讓Swift使用Foundation會更簡單一些,如下代碼所示:


let s = "hello"
let ss = (s as NSString).substringWithRange(NSMakeRange(1,3)) // "ell"  

3.7.4  Character

Character對像類型(結構體)表示單個Unicode字母,即字符串中的一個字符。可以通過characters屬性將String對像分解為一系列Character對象。形式上,它是一個String.CharacterView結構體;不過習慣稱為字符序列。如前所述,可以通過for...in遍歷字符序列來獲取String的Characters,一個接著一個:


let s = "hello"
for c in s.characters {
    print(c) // print each Character on its own line
}  

在字符序列之外遇到Character對象的情況並不多,甚至都沒有創建Character字面值的方式。要想從頭創建一個Character,請通過單字符的String進行初始化:

String與NSString元素的失配

Swift與Cocoa對字符串包含什麼元素有著不同的理解。Swift涉及字符,而NSString則涉及UTF-16編碼。每種方式都有自己的優點。相比於Swift來說,NSString速度更快,效率更高;Swift必須要遍歷字符串才能知曉字符是如何構建的;不過,Swift的做法與你的直覺是相一致的。為了強調這種差別,非字面值的Swift字符串沒有length屬性;它與NSString的length的對應之物則是其utf16屬性的count。

幸好,元素失配在實際情況中並不常見;不過,還是存在這種可能的,下面是一個測試:


let s = "Ha\u{030A}kon"
print(s.characters.count) // 5
let length = (s as NSString).length // or: s.utf16.count
print(length) // 6  

上述代碼通過一個Unicode編碼創建了一個字符串(挪威語),這個Unicode編碼與前面的編碼一同構成了一個字符,該字符上面會有一個圓圈。Swift會遍歷整個字符串,因此它會規範化這個字符串組合併返回5個字符;Cocoa只會看到該字符串包含了6個16位編碼。


let c = Character("h")  

出於同樣的原因,你可以通過一個Character初始化String。


let c = Character("h")
let s = (String(c)).uppercaseString  

可以比較Character,「小於」的含義與你的理解是一致的。

字符序列有很多方便好用的屬性與方法。由於是個集合(CollectionType),所以它擁有first與last屬性;它們都是Optional,因為字符串可能為空:


let s = "hello"
let c1 = s.characters.first // Optional("h")
let c2 = s.characters.last // Optional("o")  

indexOf方法會在序列中找到給定字符首次出現的位置並返回其索引。它也是個Optional,因為給定的字符可能在序列中並不存在:


let s = "hello"
let firstL = s.characters.indexOf("l") // Optional(2)  

所有的Swift索引都是從數字0開始的,因此2表示第3個字符。不過,這裡的索引值並不是Int;稍後將會介紹它到底是什麼以及這麼做的好處。

由於是個序列(SequenceType),字符序列有一個返回Bool的方法contains,它表示序列中是否存在某個字符:


let s = "hello"
let ok = s.characters.contains("o") // true  

此外,contains還可以接收一個函數,這個函數會接收一個Character並返回Bool(indexOf方法也可以這麼做)。如下代碼判斷目標字符串是否包含元音:


let s = "hello"
let ok = s.characters.contains {"aeiou".characters.contains($0)} // true  

filter方法接收一個函數,這個函數接收一個Character並返回Bool,它會排除掉返回false的那些字符。其結果是個字符序列,不過你可以將其強制轉換為String。如下代碼展示了如何刪除一個String中出現的所有輔音:


let s = "hello"
let s2 = String(s.characters.filter {"aeiou".characters.contains($0)}) // "eo"  

dropFirst與dropLast方法分別會返回一個排除掉第一個與最後一個字符的新字符序列:


let s = "hello"
let s2 = String(s.characters.dropFirst) // "ello"  

prefix與suffix會從初始字符序列的起始與末尾處提取出給定長度的字符序列:


let s = "hello"
let s2 = String(s.characters.prefix(4)) // "hell"  

split會根據一個函數(該函數接收一個Character並返回Bool)將字符序列轉換為數組。在如下示例中,我得到了一個String中的單詞,這裡的「單詞」指的是除了空格外的其他字符。


let s = "hello world"
let arr = s.characters.split{$0 == " "}  

不過,得到的結果是個相當奇怪的SubSlice對像數組;為了獲得String對象,我們需要使用map函數將其轉換為String。第4章將會介紹map函數,現在使用它就好了:


let s = "hello world"
let arr = split(s.characters){$0 == " "}.map{String($0)} // ["hello", "world"]  

我們還可以像操作數組那樣操作String(實際上是其底層的字符序列)。比如,你可以通過下標獲得指定位置處的字符。但遺憾的是,這其實並不是那麼容易的。比如,「hello」的第2個字符是什麼?如下代碼無法編譯通過:


let s = "hello"
let c = s[1] // compile error  

原因在於String上的索引(實際上是其字符序列上的索引)是一種特殊的嵌套類型String.Index(實際上是String.CharacterView.Index的類型別名)。創建該類型的對象並不是那麼容易的事情。首先使用String(或字符序列)的startIndex或endIndex,或indexOf方法的返回值;接下來調用advancedBy方法獲得所需的索引:


let s = "hello"
let ix = s.startIndex
let c = s[ix.advancedBy(1)] // "e"  

這種做法非常笨拙,原因在於Swift只有遍歷完序列後才能知道字符序列中的字符到底在哪裡;調用advancedBy就是為了讓Swift做到這一點。

除了advancedBy方法,還可以通過++與--來增加或是減少索引值,可以通過successor與predecessor方法得到下一個與前一個索引值。這樣,可以將上述示例修改為下面這樣:


let s = "hello"
var ix = s.startIndex
let c = s[++ix] // "e"  

也可以寫成這樣:


let s = "hello"
let ix = s.startIndex
let c = s[ix.successor] // "e"  

得到了所需的字符索引值後,你就可以通過它來修改String了。比如,insertContentsOf(at:)方法會將一個字符序列(不是String)插入String中:


var s = "hello"
let ix = s.characters.startIndex.advancedBy(1)
s.insertContentsOf("ey, h".characters, at: ix) // s is now "hey, hello"  

與之類似,removeAtIndex會刪除單個字符(並返回該字符)。

(涉及更多字符的操作需要用到Range,3.7.5節將會對其進行介紹)。

值得注意的是,我們可以將字符序列直接轉換為Character對像數組,如Array("hello".characters)。這麼做是很值得的,因為數組索引是Int,使用起來很容易。操縱完Character數組後,你可以直接將其轉換為String。3.7.5節將會介紹相關示例(第4章將會介紹數組,還會再次談及集合與序列)。

3.7.5  Range

Range對像類型(結構體)表示一對端點。有兩個運算符可以構造一個Range字面值;提供一個起始值和一個終止值,中間是一個Range運算符:

...

閉區間運算符。符號a...b表示「從a到b,包括b」。

..<

半開半閉區間運算符。符號a..<b表示「從a到b,但不包含b」。

可以在Range運算符左右兩側使用空格。

不存在反向Range:Range的起始值不能大於終止值(編譯器不會報錯,但運行時會崩潰)。

Range端點的類型通常是某種數字,大多數情況下是Int:


let r = 1...3  

如果終止值是負數,那麼必須將其放到圓括號中:


let r = -1000...(-1)  

Range的常見用法是在for...in中遍歷數字:


for ix in 1 ... 3 {
    print(ix) // 1, then 2, then 3
}  

還可以使用Range的contains實例方法判斷某個值是否在給定的範圍內;在這種情況下,Range實際上是個間隔(嚴格來說是個IntervalType):


let ix = // ... an Int ...
if (1...3).contains(ix) { // ...  

為了測試包含,Range的端點還可以是Double:


let d = // ... a Double ...
if (0.1...0.9).contains(d) { // ...  

Range的另一個常見使用場景是對序列進行索引。比如,如下代碼獲取到一個String的第2、3、4個字符。正如3.7.4節最後所介紹的那樣,我們將String的characters轉換為了一個Array;接下來將Int Range作為該數組的索引,然後再將其轉換為String:


let s = "hello"
let arr = Array(s.characters)
let result = arr[1...3]
let s2 = String(result) // "ell"  

此外,可以直接將Range作為String(或其底層字符序列)的索引,不過這時它必須是String.Index的Range,正如之前所說的,這麼做非常笨拙。更好的方式是讓Swift將從Cocoa方法調用中得到的NSRange轉換為Swift Range:


let s = "hello"
let r = s.rangeOfString("ell") // a Swift Range (wrapped in an Optional)  

還可以將Range端點作為索引值,比如,使用String startIndex的advancedBy,如前所述。得到了恰當類型的Range後,你就可以通過下標來抽取出子字符串了:


let s = "hello"
let ix1 = s.startIndex.advancedBy(1)
let ix2 = ix1.advancedBy(2)
let s2 = s[ix1...ix2] // "ell"  

一種優雅的便捷方式是從序列的indices屬性開始,它會返回一個介於序列startIndex與endIndex之間的半開Range區間;接下來就可以修改該Range並使用它了:


let s = "hello"
var r = s.characters.indices
r.startIndex++
r.endIndex--
let s2 = s[r] // "ell"  

replaceRange方法會拼接為一個範圍,這樣就可以修改字符串了:


var s = "hello"
let ix = s.startIndex
let r = ix.advancedBy(1)...ix.advancedBy(3)
s.replaceRange(r, with: "ipp") // s is now "hippo"  

與之類似,可以通過removeRange方法來刪除一系列字符:


var s = "hello"
let ix = s.startIndex
let r = ix.advancedBy(1)...ix.advancedBy(3)
s.removeRange(r) // s is now "ho"  

Swift Range與Cocoa NSRange的構建方式存在著很大的差別。Swift Range是由兩個端點定義的,Cocoa NSRange則是由一個起始點和一個長度定義的。不過,你可以將端點為Int的Swift Range轉換為NSRange,也可以通過toRange方法將NSRange轉換為Swift Range(返回一個包裝了Range的Optional)。

有時,Swift會更進一步。比如,當調用"hello".rangeOfString("ell")時,Swift會橋接Range與NSRange,它能夠正確處理好Swift與Cocoa在字符解釋與字符串長度上的差別,以及NSRange的值是Int,而描述Swift子字符串的Range端點是String.Index這些情況。

3.7.6 元組

元組是個輕量級、自定義、有序的多值集合。作為一種類型,它是通過一個圓括號,裡面是所含值的類型,類型之間通過逗號分隔來表示的。比如,下面是一個包含Int與String的元組類型變量的聲明:


var pair : (Int, String)  

元組字面值的表示方式也是一樣的,圓括號中是所包含的值,值與值之間通過逗號分隔:


var pair : (Int, String) = (1, "One")  

這些類型可以推導出來,因此沒必要在聲明中顯式指定類型:


var pair = (1, "One")  

元組是純粹的Swift語言特性,它們與Cocoa和Objective-C並不兼容,因此只能將其用在Cocoa無法觸及之處。不過在Swift中,它們有很多用武之地。比如,元組顯然就是函數只能返回一個值這一問題的解決之道;元組本身是一個值,但它可以包含多個值,因此將元組作為函數的返回類型可以讓函數返回多個值。

元組具有很多語言上的便捷性,你可以賦值給變量名元組,以此作為同時給多個變量賦值的一種方式:


var ix: Int
var s: String
(ix, s) = (1, "One")  

這麼做非常方便,Swift可以在一行完成對多個變量同時初始化的工作:


var (ix, s) = (1, "One") // can use let or var here  

可以通過元組安全地實現變量值的互換:


var s1 = "Hello"
var s2 = "world"
(s1, s2) = (s2, s1) // now s1 is "world" and s2 is "Hello"  

全局函數swap能以更加通用的方式實現值的互換。

要想忽略掉其中一個賦值,請在接收元組中使用下劃線表示:


let pair = (1, "One")
let (_, s) = pair // now s is "One"  

enumerate方法可以通過for...in遍歷序列,然後在每次迭代中接收到每個元素的索引號與元素本身;這兩個結果是以元組的形式返回的:


let s = "hello"
for (ix,c) in s.characters.enumerate {
    print("character \(ix) is \(c)")
}  

我之前曾指出過,addWithOverflow等數字的實例方法會返回一個元組。

可以直接引用元組的每個元素。第1種方式是通過索引號,將字面數字(不是變量值)作為消息名發送給元組,並使用點符號:


let pair = (1, "One")
let ix = pair.0 // now ix is 1  

如果對元組的引用不是常量,那麼可以通過相同手段為其賦值:


var pair = (1, "One")
pair.0 = 2 // now pair is (2, "One")  

訪問元組元素的第2種方式是給元組命名,這類似於函數參數,並且要作為顯式或隱式類型聲明的一部分。下面是創建元組元素名的一種方式:


let pair : (first:Int, second:String) = (1, "One")  

下面是另一種方式:


let pair = (first:1, second:"One")  

名字現在是該值類型的一部分,並且要通過隨後的賦值來訪問。接下來可以將其用作字面消息名,就像數字字面值一樣:


var pair = (first:1, second:"One")
let x = pair.first // 1
pair.first = 2
let y = pair.0 // 2  

可以將沒有名字的元組賦給相應的有名字的元組,反之亦然:


let pair = (1, "One")
let pairWithNames : (first:Int, second:String) = pair
let ix = pairWithNames.first // 1  

在傳遞或是從函數返回一個元組時可以省略元組名:


func tupleMaker -> (first:Int, second:String) {
    return (1, "One") // no names here
}
let ix = tupleMaker.first // 1  

如果在程序中會一以貫之地使用某種類型的元組,那麼為它起個名字就很有必要了。要想做到這一點,請使用Swift的typealias關鍵字。比如,在我開發的LinkSame應用中有一個Board類,它描述並且操縱著遊戲格局。Board是由Piece對像構成的網格,我需要通過一種方式來描述網格的位置,它是一對整型,因此將其定義為元組:


class Board {
    typealias Point = (Int,Int)
    // ...
}  

這麼做的好處在於現在在代碼中可以輕鬆使用Point了。比如,給定一個Point,我可以獲取到相應的Piece:


func pieceAt(p:Point) -> Piece? {
    let (i,j) = p
    // ... error-checking goes here ...
    return self.grid[i][j]
}  

擁有元素名的元組與函數參數列表之間的相似性並非巧合。參數列表就是個元組!事實上,每個函數都接收一個元組參數並返回一個元組。這樣就可以向接收多個參數的函數傳遞單個元組了。比如,一個函數如下代碼所示:


func f (i1:Int, _ i2:Int) ->  {}  

f的參數列表是個元組。這樣,調用f時就可以將元組作為實參傳遞進去了:


let tuple = (1,2)
f(tuple)  

在該示例中,f沒有外部參數名。如果函數有外部參數名,那麼你可以向其傳遞一個帶有具名元素的元組。如下面這個函數:


func f2 (i1 i1:Int, i2:Int) ->  {}  

可以像下面這樣調用:


let tuple = (i1:1, i2:2)
f2(tuple)  

不過,出於我也尚不清楚的一些原因,以這種方式作為函數參數傳遞的元組必須是常量。如下代碼將無法編譯通過:


var tuple = (i1:1, i2:2)
f2(tuple) // compile error  

與之類似,Void(不返回值的函數所返回的值類型)實際上是空元組的類型別名,這也是可以將其寫成()的原因所在。

3.7.7  Optional

Optional對像類型(枚舉)用於包裝任意類型的其他對象。單個Optional對像只能包裝一個對象。此外,一個Optional對像還可能不包裝任何對象。這正是Optional這個名字的由來,即可選:它可以包裝其他對象,也可以不包裝。你可以將Optional看作一種盒子,這個盒子可能是空的。

首先創建包裝一個對象的Optional。假設我們需要一個包裝了字符串"howdy"的Optional,一種創建方式就是使用Optional初始化器:


var stringMaybe = Optional("howdy")  

如果使用print將stringMaybe的值輸出到控制台上,那麼我們會看到與相應的初始化器Optional("howdy")相同的表達式。

在聲明與初始化後,stringMaybe就擁有了類型,它既不是String,也不是簡單的Optional,實際上它是包裝了String的Optional。這意味著只能將包裝了String的Optional(而不能是包裝了其他類型的Optional)賦給它。如下代碼是合法的:


var stringMaybe = Optional("howdy")
stringMaybe = Optional("farewell")  

如下代碼則是不合法的:


var stringMaybe = Optional("howdy")
stringMaybe = Optional(123) // compile error  

Optional(123)是一個包裝了Int的Optional,如果需要包裝了String的Optional,那麼你無法將其賦給它。

Optional對於Swift非常重要,因此語言本身提供了使用它的特殊語法。創建Optional的常規方法並不是使用Optional初始化器(當然了,你可以這麼做),而是將某個類型的值賦給或是傳遞給包裝該類型的Optional引用。比如,如果stringMaybe的類型是包裝了String的Optional,那麼你可以直接將字符串賦給它。這麼做貌似不合法,但實際上卻是可以的。結果就是被賦值的String被自動包裝到了那個Optional中:


var stringMaybe = Optional("howdy")
stringMaybe = "farewell" // now stringMaybe is Optional("farewell")  

我們還需要一種方式能夠顯式地將某個變量聲明為包裝了String的Optional;否則就無法聲明Optional類型的變量了,同時也無法聲明Optional類型的參數。本質上,Optional是個泛型,因此包裝了String的Optional其實是Optional<String>(第4章將會介紹該語法)。不過,你不用非得這麼寫。Swift語言支持Optional類型表示的語法糖:使用包裝類型名,後跟一個問號。比如:


var stringMaybe : String?  

這樣就完全不需要使用Optional初始化器了。我可以將變量聲明為包裝String的Optional,然後將一個String賦給它進行包裝,一步就能搞定:


var stringMaybe : String? = "howdy"  

事實上,這才是在Swift中創建Optional的常規方式。

在得到了包裝某個具體類型的Optional後,你可以將其用在需要包裝該類型的Optional的場合中,就像其他任何值一樣。如果函數參數是一個包裝了String的Optional,那就可以將stringMaybe作為實參傳遞給該參數:


func optionalExpecter(s:String?) {}
let stringMaybe : String? = "howdy"
optionalExpecter(stringMaybe)  

此外,在需要包裝某個類型值的Optional時,你可以將被包裝類型的值傳遞進去。這是因為參數傳遞就像是賦值:未包裝的值會被隱式包裝。比如,如果函數需要一個包裝了String的Optional,那麼你可以傳遞一個String實參,它會在接收參數中被包裝為Optional:


func optionalExpecter(s:String?) {
    // ... here, s will be an Optional wrapping a String ...
    print(s)
}
optionalExpecter("howdy") // console prints: Optional("howdy")  

但反過來則不行,你不能在需要被包裝類型的地方使用包裝該類型的Optional,這麼做將無法編譯通過:


func realStringExpecter(s:String) {}
let stringMaybe : String? = "howdy"
realStringExpecter(stringMaybe) // compile error  

錯誤消息是:「Value of optional type Optional<String>not unwrapped;did you mean to use!or??」。你經常會在Swift中看到這類消息!正如消息所表示的,如果需要被Optional包裝的類型,但使用的卻是Optional,那就需要展開Optional;也就是說,你需要進入Optional中,取出它包裝的實際內容。下面就來介紹如何做到這一點。

1.展開Optional

之前已經介紹過將對像包裝到Optional中的多種方法。不過相反的過程會怎樣呢?如何展開Optional得到其中的對象呢?一種方式是使用展開運算符(或是強制展開運算符),它是個後綴感歎號,如下代碼所示:


func realStringExpecter(s:String) {}
let stringMaybe : String? = "howdy"
realStringExpecter(stringMaybe!)  

在上述代碼中,stringMaybe!語法表示進入Optional stringMaybe中,獲取被包裝的值,然後在該處使用這個值。由於stringMaybe是個包裝了String的Optional,因此裡面的內容就是個String。這正是realStringExpecter函數的參數類型!因此,我們可以將展開的Optional作為實參傳遞給realStringExpecter。stringMaybe是個包裝了String"howdy"的Optional,不過stringMaybe!卻是String"howdy"。

如果Optional包裝了某個類型,那麼你無法向其發送該類型所允許的消息;首先需要展開它。比如,我們想要獲得stringMaybe的大寫形式:


et stringMaybe : String? = "howdy"
let upper = stringMaybe.uppercaseString // compile error  

解決方法就是展開stringMaybe獲得裡面的String。可以通過展開運算符直接達成所願:


let stringMaybe : String? = "howdy"
let upper = stringMaybe!.uppercaseString  

如果需要使用Optional多次來獲得其中包裝的類型,並且每次都需要使用展開運算符獲取裡面的對象,那麼代碼很快就會變得非常冗長。比如,在iOS編程中,應用的窗口就是應用委託的Optional UIWindow屬性(self.window):


// self.window is an Optional wrapping a UIWindow
self.window = UIWindow
self.window!.rootViewController = RootViewController
self.window!.backgroundColor = UIColor.whiteColor
self.window!.makeKeyAndVisible  

這麼做太笨拙了,立刻可以想到的一種解決辦法就是將展開值賦給包裝類型的一個變量,然後使用該變量即可:


// self.window is an Optional wrapping a UIWindow
self.window = UIWindow
let window = self.window!
// now window (not self.window) is a UIWindow, not an Optional
window.rootViewController = RootViewController
window.backgroundColor = UIColor.whiteColor
window.makeKeyAndVisible  

其實還有別的方法,現在就來介紹一下。

2.隱式展開Optional

Swift提供了在需要被包裝類型時使用Optional的另一種方式:你可以將Optional類型聲明為隱式未包裝的。這其實是另一種類型,即ImplicitlyUnwrappedOptional。ImplicitlyUnwrappedOptional是一種Optional,不過編譯器允許它使用一些特殊的魔法操作:在需要被包裝類型時,可以直接使用它。你可以顯式展開ImplicitlyUnwrappedOptional,但不必這麼做,因為它可以隱式展開(這也是其名字的由來)。比如:


func realStringExpecter(s:String) {}
var stringMaybe : ImplicitlyUnwrappedOptional<String> = "howdy"
realStringExpecter(stringMaybe) // no problem  

與Optional一樣,Swift提供了語法糖來表示隱式展開的Optional類型。就像包裝了String的Optional可以表示為String?一樣,包裝了String的隱式展開Optional可以表示為String!。這樣,我們可以將上述代碼重寫為(這也是實際開發中的寫法):


func realStringExpecter(s:String) {}
var stringMaybe : String! = "howdy"
realStringExpecter(stringMaybe)  

請記住,隱式展開的Optional也是個Optional,它只是個便捷的寫法而已。通過將對像聲明為隱式展開的Optional,你告訴編譯器,如果在需要被包裝類型的地方使用了它,那麼編譯器能夠將其展開。

就它們的類型來說,常規Optional會包裝某個類型(如String?),而隱式展開的Optional也包裝了相同的類型(如String!),它們之間是可以互換的:在需要其中一個的地方都可以使用另外一個。

3.魔法詞nil

我一直在說Optional會包含一個包裝值,不過不包含任何包裝值的Optional是什麼呢?正如我之前所說的,這種Optional也是合法的實體;事實上,這兩種情況構成了完整的Optional。

你需要通過一種方式來判斷一個Optional是否包含了包裝值,以及指定沒有包裝值的Optional。Swift讓這一切變得異常簡單,這是通過一個特殊的關鍵字nil來實現的:

判斷一個Optional是否包含了包裝值

測試Optional是否與nil相等。如果相等,那麼該Optional就是空的。一個空的Optional在控制台中也會打印出nil。

指定沒有包裝值的Optional

在需要Optional類型時賦值或傳遞一個nil,結果就是期望類型的Optional,它不包含包裝值。

比如:


var stringMaybe : String? = "Howdy"
print(stringMaybe) // Optional("Howdy")
if stringMaybe == nil {
    print("it is empty") // does not print
}
stringMaybe = nil
print(stringMaybe) // nil
if stringMaybe == nil {
    print("it is empty") // prints
}  

魔法詞nil可以表達這個概念:一個Optional包裝了恰當的類型,但實際上不包含該類型的任何對象。顯然,這是非常方便的;你可以充分利用它。不過重要的是,你要理解它只是個魔法而已:Swift中的nil並不是對象,也不是值。它只不過是個簡便寫法而已。你可以認為這個簡便寫法就是真正存在的。比如,我可以說某個東西是nil。但實際上,沒有什麼東西會是nil;nil並不是具體的事物。我的意思是這個東西相當於nil(因為它是個沒有包裝任何東西的Optional)。

沒有包裝對象的Optional的實際值是Optional.None,包裝了String的Optional裡面如果沒有String對象,那麼其實際值是Optional<String>.None。不過在實際開發中,你是不需要這麼編寫代碼的,因為只需寫成nil即可。第4章將會介紹這些表達式的真正含義。

由於類型為Optional的變量可能為nil,所以Swift使用了一種特殊的初始化規則:如果變量(var)的類型為Optional,那麼其值自動就為nil。如下代碼是合法的:


func optionalExpecter(s:String?) {}
var stringMaybe : String?
optionalExpecter(stringMaybe)  

上述代碼很有趣,因為看起來好像是不合法的。我們聲明了一個變量stringMaybe,但卻沒有給它賦值。不過卻將其傳遞給了一個函數,就好像它是有值一樣。這是因為它的的確確是有值的。該變量會被隱式初始化為nil。在Swift中,類型為Optional的變量(var)是唯一一種會被隱式初始化的變量類型。

現在來談談也許是Swift中最為重要的一個原則:不能展開不包含任何東西的Optional(即等於nil的Optional)。這種Optional不包含任何東西;沒有什麼需要展開的。事實上,顯式展開不包含任何東西的Optional會造成程序在運行時崩潰。


var stringMaybe : String?
let s = stringMaybe! // crash  

崩潰消息的內容是:「Fatal error:unexpectedly found nil while unwrapping an Optional value」。習慣吧,因為你會經常看到這個消息。這是個很容易犯的錯誤。事實上,展開一個不包含值的Optional可能是導致Swift程序崩潰最常見的一個原因,你應該好好利用這種崩潰的情況。事實上,如果某個Optional中不包含值,那麼你希望應用崩潰,因為這個Optional本應該包含值的,既然不包含值,那就說明其他地方出錯了。

要想消除這種崩潰的情況,你需要確保Optional中包含值,如果不包含,那麼請不要將其展開。顯而易見的一種做法是首先將其與nil進行比較:


var stringMaybe : String?
// ... stringMaybe might be assigned a real value here ...
if stringMaybe != nil {
    let s = stringMaybe!
    // ...
}  

4.Optional鏈

有時,你想向被Optional所包裝的值發送消息。要想做到這一點,你可以將Optional展開。如下面這個示例:


let stringMaybe : String? = "howdy"
let upper = stringMaybe!.uppercaseString  

這種形式的代碼叫作Optional鏈。在點符號鏈的中間,你已經將Optional展開了。

如果不展開,那就無法向Optional發送消息。Optional本身並不會響應任何消息(實際情況是,它們會響應一些消息,不過非常少,你基本上不會用到——它們也不是Optional裡面的對象所要響應的消息)。如果向Optional發送了本該發送給裡面的對象的消息,那麼編譯器就會報錯:


let stringMaybe : String? = "howdy"
let upper = stringMaybe.uppercaseString // compile error  

不過,我們已經看到,如果展開一個不包含對象的Optional,那麼應用將會崩潰。這樣,如果不確定一個Optional是否包含了對象該怎麼辦呢?在這種情況下,如何向一個Optional發送消息呢?Swift針對這個目的提供了一個特殊的簡寫形式。要想安全地向可能為空的Optional發送消息,你可以展開這個Optional。在這種情況下,請通過問號後綴運算符而非感歎號將Optional展開:


var stringMaybe : String?
// ... stringMaybe might be assigned a real value here ...
let upper = stringMaybe?.uppercaseString  

這是個Optional鏈,你通過問號展開了該Optional。通過使用該符號,你可以有條件地將Optional展開。條件就是一種安全保障;會幫助我們執行與nil的比較。代碼表示的意思是:如果stringMaybe包含了一個String,那麼將其展開並向其發送uppercaseString消息;如果不包含(也就是說等於nil),那就不要展開它,也不要向其發送任何消息。

這種代碼是個雙刃劍。一方面,如果stringMaybe為nil,那麼應用在運行期不會崩潰;另一方面,如果stringMaybe為nil,那麼這一行代碼其實什麼都沒做,並不會得到任何大寫字符串。

不過現在又有了一個新問題。在上述代碼中,我們使用一個表達式(該表達式會發送uppercaseString消息)初始化了變量upper。結果卻是uppercaseString這條消息可能發送了,也可能根本就沒有發送。那麼,upper被初始化成了什麼呢?

為了處理這種情況,Swift有一個特殊的原則。如果一個Optional鏈包含了可選的展開Optional,並且如果該Optional鏈生成了一個值,那麼該值本身就會被包裝到Optional中。這樣,upper的類型就是包裝了String的Optional。這麼做非常棒,因為它涵蓋了兩種可能的情況。首先,假設stringMaybe包含了一個String:


var stringMaybe : String?
stringMaybe = "howdy"
let upper = stringMaybe?.uppercaseString // upper is a String?  

上述代碼執行後,upper並不是一個String;它不是"HOWDY"。實際上,它是個包裝了"HOWDY"的Optional!另一方面,如果嘗試展開Optional的操作失敗了,那麼該Optional鏈會返回nil:


var stringMaybe : String?
let upper = stringMaybe?.uppercaseString // upper is a nil String?  

以這種方式展開Optional是優雅且安全的;不過請考慮一下執行結果。一方面,即便stringMaybe是nil,應用也不會在運行時崩潰。另一方面,這麼做並不比之前的做法更好:我們實際上得到了另一個Optional!無論stringMaybe是否為nil,upper的類型都是一個包裝了String的Optional,為了使用其中的String,你需要展開upper。我們不知道upper是否為nil,因此會遇到與之前一樣的問題——需要確保能夠安全展開upper,並且不會意外展開一個空的Optional。

更長的Optional鏈也是合法的。它們的工作方式與你想像的完全一致:無論鏈中要展開多少個Optional,如果其中一個被展開了,那麼整個表達式就會生成一個Optional,它包裝的是Optional被正常展開後所得到的類型,並且在這個過程中會安全地失敗。比如:


// self.window is a UIWindow?
let f = self.window?.rootViewController?.view.frame  

視圖的frame屬性是個CGRect。不過在上述代碼執行後,f並非CGRect,它是個包裝了CGRect的Optional。如果鏈中的任何一個展開失敗了(由於要展開的Optional為nil),那麼整個鏈就會返回nil以表示失敗。

注意到上述代碼並沒有嵌套使用Optional;並不會因為鏈中有兩個Optional就生成包裝到Optional中的CGRect,然後這個Optional又包裝到另一個Optional中。不過,出於其他一些原因,我們可以生成包裝到另一個Optional中的Optional,第4章將會給出一個示例。

如果涉及可選展開Optional的Optional鏈生成了一個結果,那麼你可以通過檢查結果來判斷鏈中的所有Optional是否可以安全展開:如果它不為nil,那麼一切都可以成功展開。但如果沒有得到結果呢?比如:


self.window?.rootViewController = UIViewController  

現在真是進退維谷。程序當然是不會崩潰的了;如果self.window為nil,那麼它不會展開,因此安全。但如果self.window為nil,我們也沒辦法為窗口賦一個根視圖控制器了!最好要知道該Optional鏈的展開是否是成功的。幸好,我們可以通過一個技巧來實現這個目標。在Swift中,不返回值的語句都會返回一個Void。因此,對擁有可選展開的Optional的賦值會返回一個包裝了Void的Optional;你可以捕獲到這個Optional,這意味著你可以判斷它是否為nil;如果不為nil,那麼賦值就成功了。比如:


let ok : Void? = self.window?.rootViewController = UIViewController
if ok != nil {
    // it worked
}  

顯然,無須顯式地將包裝了Void的Optional賦給變量;你可以在一步中完成捕獲和與nil的比較兩件事:


if (self.window?.rootViewController = UIViewController) != nil {
    // it worked
}  

如果函數調用返回一個Optional,那麼你可以展開結果並使用,無須先捕獲結果,可以直接展開,方式是在函數調用後使用一個感歎號或問號(即在右圓括號後面)。這與之前所做的別無二致,只不過相對於Optional屬性或變量來說,這裡使用的是返回Optional的函數調用。比如:


class Dog {
    var noise : String?
    func speak -> String? {
        return self.noise
    }
}
let d = Dog
let bigname = d.speak?.uppercaseString  

最後不要忘記,bigname並非String,它是個包裝了String的Optional。

第5章介紹流程控制時還會繼續介紹檢查Optional是否為nil的其他Swift語法。

!與?後綴運算符(分別表示無條件與有條件展開Optional)和表示Optional類型時與類型名搭配使用的!和?語法糖(如String?表示包裝了String的Optional,String!表示隱式展開包裝了String的Optional)沒有任何關係。二者之間表面上的相似性迷惑了很多初學者。

5.與Optional的比較

在與除nil的其他值比較時,Optional會特殊一些:比較的是包裝值而非Optional本身。比如,如下代碼是合法的:


let s : String? = "Howdy"
if s == "Howdy" { // ... they _are_ equal!  

上述代碼看起來不可行,但實際上卻是可行的;展開一個Optional,但卻只是為了將其包裝值與其他值進行比較,這麼做非常麻煩(特別是,你還得先檢查Optional是否為nil)。相對於將Optional本身與"Howdy"進行比較,Swift會自動(且安全)將其包裝值(如果有)與"Howdy"比較,而且比較成功了。如果被包裝值不是"Howdy",那麼比較就會失敗。如果沒有被包裝值(s為nil),那麼比較也會失敗,這非常安全!這樣,你就可以將s與nil或String進行比較了,在所有情況下比較都可以正確進行。

同樣地,如果Optional包裝了可以使用大於和小於運算符類型的值,那麼這些運算符也可以直接應用到Optional上:


let i : Int? = 2
if i < 3 { // ... it _is_ less!  

6.為何使用Optional?

既然已經知道如何使用Optional,那麼你可能想知道為何要使用Optional。Swift為何要提供Optional,好處又是什麼呢?

Optional一個非常重要的目的就是提供可與Objective-C交換的對象值。在Objective-C中,任何對像引用都可能為nil。因此需要通過一種方式向Objective-C發送nil並接收來自Objective-C的nil。Swift Optional就提供了這一方式。

Swift會幫助你正確使用Cocoa API中的恰當類型。比如,考慮UIView的backgroundColor屬性,它是個UIColor,不過可能為nil,你也可以將其設為nil。這樣,其類型就是UIColor?。為了設置這個值,你無須直接使用Optional。請記住,將被包裝值賦給Optional是合法的,因為系統會將其包裝起來。這樣,你可以將myView.backgroundColor設為UIColor或nil。不過,如果獲得了UIView的backgroundColor,那麼你就有了一個包裝UIColor的Optional,你需要清楚這一事實,否則就可能出現奇怪的結果:


let v = UIView
let c = v.backgroundColor
let c2 = c.colorWithAlphaComponent(0.5) // compile error  

上述代碼向c發送了colorWithAlphaComponent消息,就好像它是個UIColor一樣。它其實不是UIColor,而是包裝了UIColor的Optional。Xcode會在這種情況下幫助你;如果使用代碼完成輸入了colorWithAlphaComponent方法的名字,那麼Xcode會在c後面插入一個問號,這樣就會展開Optional並得到合法的代碼:


let v = UIView
let c = v.backgroundColor
let c2 = c?.colorWithAlphaComponent(0.5)  

不過在大多數情況下,Cocoa對像類型都不會被標記為Optional。這是因為,雖然從理論上說,它可能為nil(因為任何Objective-C對像引用都可能為nil);但實際上,它不會。Swift會將值看作對像類型本身。這個魔法是通過對Cocoa API(又叫作審計)採取一定的處理實現的。在Swift早期公開版中(2014年6月),從Cocoa接收到的所有對象值實際上都是Optional類型(通常是隱式展開的Optional)。不過後來,Apple花了大力氣調整API,從而去除了那些不需要作為Optional的Optional。

在一些情況下,你還是會遇到來自於Cocoa的隱式展開Optional。比如,在本書編寫之際,NSBundle方法loadNibNamed:owner:options:的API如下代碼所示:


func loadNibNamed(name: String!,
    owner: AnyObject!,
    options: [NSObject : AnyObject]!)
    -> [AnyObject]!  

這些隱式展開Optional表明這個頭文件還沒有被處理過。它們無法精確表示出現狀(比如,你永遠不會將nil作為第一個參數傳遞進去),不過問題倒也不太大。

使用Optional的另一個重要目的在於推斷出實例屬性的初始化。如果變量(通過var聲明)類型是Optional,那麼即便沒有對其初始化,它也會有一個值,即nil。如果你知道某個對象將會具有值,但不是現在,那麼Optional就非常方便了。在實際的iOS編程中,一個典型示例就是插座變量,它是指向界面中某個東西(如按鈕)的一個引用:


class ViewController: UIViewController {
    @IBOutlet var myButton: UIButton!
    // ...
}  

現在可以忽略@IBOutlet指令,它是對Xcode的一個內部提示(第7章將會介紹)。重要之處在於屬性myButton在ViewController實例首次創建出來之後還沒有值,但在視圖控制器的視圖加載後,myButton值會被設定好,這樣它就會指向界面中實際的UIButton對象了。因此,該變量的類型是個隱式展開Optional。之所以是Optional,是因為當ViewController實例首次創建出來後,myButton需要一個佔位符值。它是個隱式展開Optional,這樣代碼就可以將self.myButton當作對實際的UIButton的引用,不必強調它是個Optional了。

另一種相關情況是當一個變量(通常是實例屬性)所表示的數據需要一些時間才能獲取到該怎麼辦。比如,在我寫的Albumen應用中,當應用啟動時,我會創建一個根視圖控制器的實例。我還想獲取用戶音樂庫的數據,然後將數據存儲到根視圖控制器實例的實例屬性中。不過獲取這些數據需要時間。因此,需要先實例化根視圖控制器,然後再獲取數據,因為如果在實例化根視圖控制器之前獲取數據,那麼應用的啟動時間就會很長,這種延遲會很明顯,甚至可能會造成應用崩潰(因為iOS不允許過長的啟動時間)。因此,數據屬性都是Optional類型的;在獲取到數據之前,它們都是nil;當數據獲取到後,它們才會被賦予「真正的」值:


class RootViewController : UITableViewController {
    var albums : [MPMediaItemCollection]! = nil // initialized to nil
    // ...  

最後,Optional最重要的用處之一就是可以將值標記為空或使用不正確的值,上述代碼已經很好地說明了這一點。當Albumen應用啟動時,它會顯示出一個表格,列出了用戶所有的音樂專輯。不過在啟動時,數據尚未取得。展示表格的代碼會檢查albums是否為nil;如果是,那就顯示一個空的表格。在獲取到數據後,表格會再一次展示數據。這次,展示表格的代碼會發現albums不為nil,而是包含了實際的數據,它現在就會將數據顯示出來。借助Optional,albums可以存儲數據,也可以表示其中沒有數據。

很多內建的Swift函數都以類似的方式使用Optional,比如,之前提到的將String轉換為Int:


let s = "31"
let i = Int(s) // Optional(31)  

從String初始化Int會返回一個Optional,因為轉換可能會失敗。如果s是"howdy",那麼它就不是數字。這樣,返回的類型就不是Int,因為沒有一個Int可以表示「我沒有找到Int」這一含義。返回一個Optional優雅地解決了這一問題:nil表示我沒有找到Int,否則實際的Int結果就會位於Optional所包裝的對象中。

Swift在這方面要比Objective-C更加聰明。如果引用是個對象,那麼Objective-C可以返回nil來報告失敗;不過在Objective-C中,並非一切都是對象。因此,很多重要的Cocoa方法都會返回一個特殊值來表示失敗,你需要知道這一點,並記得對其進行測試。比如,NSString的rangeOfString:可能在目標字符串中找不到給定的子字符串;在這種情況下,它會返回一個長度為0的NSRange,其位置(索引)是個特殊值,即NSNotFound,它實際上是個非常大的負數。幸好,這種特殊值已經內建在Swift對Cocoa API的橋接中:Swift會將返回值的類型作為包裝了Range的Optional,如果rangeOfString:返回一個NSRange,其位置是NSNotFound,那麼Swift會將其表示為nil。

不過,並非Swift–Cocoa橋接的每一處都如此便捷。如果調用了NSArray的indexOfObject:,那麼結果就是個Int,而不是包裝了Int的Optional;結果還有可能是NSNotFound,你要記得對其進行測試:


let arr = [1,2,3]
let ix = (arr as NSArray).indexOfObject(4)
if ix == NSNotFound { // ...  

另一種做法是使用Swift,然後調用indexOf方法,它會返回一個Optional:


let arr = [1,2,3]
let ix = arr.indexOf(4)
if ix == nil { // ...