首页 » iOS编程基础:Swift、Xcode和Cocoa入门指南 » iOS编程基础:Swift、Xcode和Cocoa入门指南全文在线阅读

《iOS编程基础:Swift、Xcode和Cocoa入门指南》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 == .Compactif 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 = 10let x = Double(i)print(x) // 10.0, a Doublelet y = 3.8let j = Int(y)print(j) // 3, an Int  

在将数字值赋给变量或作为参数传递给函数时,Swift只会执行字面值的隐式转换。如下代码是合法的:


let d : Double = 10  

不过如下代码是非法的,因为你所赋予的变量(非字面值)是另外一种类型;编译器会阻止你这么做:


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

解决办法就是在赋值或传递变量时进行显式转换:


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

使用算术运算合并数字值时也需要遵循该原则。Swift只会执行隐式转换。常见的情况是对Int与Double执行算术运算;Int会被当作Double:


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

不过,如果对不同数字类型的变量执行算术运算,这些变量需要进行显式转换,这样才能确保它们都是相同类型。比如:


let i = 10let n = 3.0let 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.CGImagelet 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.widthlet 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 - 2let j = i + 12/2 // crash  

在某些情况下,你希望强制这种操作能够成功,因此需要提供特殊的上溢/下溢方法。这些方法会返回一个元组;虽然还没有介绍过元组,但是我还是打算展示这样一个示例:


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

现在,j值为Int.min+3(因为其值已经由原来的对Int.max的包装变成了对Int.min的包装),over值为true(用于报告溢出情况)。

如果你对是否存在上溢/下溢的情况不在乎,那么可以通过特殊的算术运算符来消除错误:&+、&-和&*。

你常常会将现有变量值与另一个值合并起来,然后将结果存储到相同的变量中。请记住,为了做到这一点,你需要将变量声明为var:


var i = 1i = i + 7  

作为一种简便写法,你可以通过一个运算符一步完成算术运算与赋值:


var i = 1i += 7  

简便(复合)赋值算术运算符有+=、-=、*=、/=、%=、&=、|=、^=、~=、<<=和>>=。

我们常常需要将某个数值加1或减1,Swift提供了一元增加与减少运算符++和--。区别在于它们用作前缀还是后缀。如果用作前缀(++i、--i),那么值就会增加或减少,并存储到相同的变量中,然后用于外部表达式中;如果用作后缀(i++、i--),那么变量当前值就会用在外部表达式中,然后值再增加或减少,并存储到相同变量中。显然,变量必须要通过var声明才可以。

运算优先级也是非常直观的:比如,*的优先级比+要高,因此x+y*z会先执行y*z,然后再将结果与x相加。如果有问题,可以通过圆括号消除歧义;比如,(x+y)*z就会先执行加法操作。

全局函数包含了abs(取绝对值)、max和min:


let i = -7let j = 6print(abs(i)) // 7print(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 = 5let s = "You have /(n) widgets."  

现在,s表示字符串“You have 5 widgets”。该示例本身没什么太大价值,因为我们知道n是什么,并且可以直接在字符串中输入5;不过,如果我们不知道n是什么呢!此外,转义圆括号中的内容不一定非得是变量的名字;它可以是Swift中任何合法的表达式。如果不知道怎么用,如下示例会更具价值:


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

转义圆括号中不能有双引号。这令人感到失望,但却不是什么障碍;这时只需将其赋给一个变量,然后在圆括号中使用该变量即可。比如,你不能这么做:


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

对双引号转义也无济于事,你只能写成多行,如下代码所示:


let ud = NSUserDefaults.standardUserDefaultslet 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 = 7let s = String(i) // "7"  

字符串还可以通过其他进制来表示Int,提供一个radix:参数来表示进制:


let i = 31let 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) // 5let length = (s as NSString).length // or: s.utf16.countprint(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.startIndexlet c = s[ix.advancedBy(1)] // "e"  

这种做法非常笨拙,原因在于Swift只有遍历完序列后才能知道字符序列中的字符到底在哪里;调用advancedBy就是为了让Swift做到这一点。

除了advancedBy方法,还可以通过++与--来增加或是减少索引值,可以通过successor与predecessor方法得到下一个与前一个索引值。这样,可以将上述示例修改为下面这样:


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

也可以写成这样:


let s = "hello"let ix = s.startIndexlet 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.indicesr.startIndex++r.endIndex--let s2 = s[r] // "ell"  

replaceRange方法会拼接为一个范围,这样就可以修改字符串了:


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

与之类似,可以通过removeRange方法来删除一系列字符:


var s = "hello"let ix = s.startIndexlet 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: Intvar 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 // 1pair.first = 2let y = pair.0 // 2  

可以将没有名字的元组赋给相应的有名字的元组,反之亦然:


let pair = (1, "One")let pairWithNames : (first:Int, second:String) = pairlet 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 UIWindowself.window = UIWindowself.window!.rootViewController = RootViewControllerself.window!.backgroundColor = UIColor.whiteColorself.window!.makeKeyAndVisible  

这么做太笨拙了,立刻可以想到的一种解决办法就是将展开值赋给包装类型的一个变量,然后使用该变量即可:


// self.window is an Optional wrapping a UIWindowself.window = UIWindowlet window = self.window!// now window (not self.window) is a UIWindow, not an Optionalwindow.rootViewController = RootViewControllerwindow.backgroundColor = UIColor.whiteColorwindow.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 = nilprint(stringMaybe) // nilif 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 = UIViewControllerif 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 = Doglet 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? = 2if 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 = UIViewlet c = v.backgroundColorlet c2 = c.colorWithAlphaComponent(0.5) // compile error  

上述代码向c发送了colorWithAlphaComponent消息,就好像它是个UIColor一样。它其实不是UIColor,而是包装了UIColor的Optional。Xcode会在这种情况下帮助你;如果使用代码完成输入了colorWithAlphaComponent方法的名字,那么Xcode会在c后面插入一个问号,这样就会展开Optional并得到合法的代码:


let v = UIViewlet c = v.backgroundColorlet 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 { // ...