本文系阅读阅读原章节后总结概括得出。由于需要我进行一定的概括提炼,如有不当之处欢迎读者斧正。如果你对内容有任何疑问,欢迎共同交流讨论。
可选绑定
我们可以利用if let
语法进行可选绑定:
if let idx = array.indexOf("four") {
array.removeAtIndex(idx)
}
复制代码
这里的let
表示变量idx
不可被修改,如果它需要修改,可以用var
,但是这个变量是原来变量的拷贝,对它的修改不会影响到原来的变量。
可选绑定的if
可以有where
从句。比如可以这样移除数组的第一个元素:
if let idx = array.indexOf("four")
where idx != array.startIndex {
array.removeAtIndex(idx)
}
复制代码
可选绑定可以用多个从句,后面的变量依赖于前面的可选绑定成功进行,否则整个if语句就会终止:
let string = "http://www.google.com/images/srpr/logo11w.png"
if let url = NSURL(string: string),
data = NSData(contentsOfURL: url),
image = NSImage(data: data)
{
let view = UIImageView(image: image)
XCPShowView("Download image", view: view)
}
复制代码
多个变量的每个部分都可以有一个where
从句:
if let url = NSURL(string: string) where url.pathExtension == "png" ,
data = NSData(contentsOfURL: url), image = NSImage(data: data)
{
let view = UIImageView(image: image)
}
复制代码
我们可以在在if
语句中进行可选绑定,也可以在while
循环中进行可选绑定。这表示一个循环,这个循环只能在返回nil
时结束:
let array = [1,2,3]
var generator = array.generate()
while let i = generator.next() {
print(i)
}
复制代码
双层嵌套可选类型
可选类型包装的值当然可以还是一个可选类型,这就是可选类型的嵌套。假设有一个字符串数组,我们把数组中的字符串转换成数字,可以用map
方法实现:
let stringNumbers = ["1", "2", "3", "foo"]
let maybeInts = stringNumbers.map{ Int($0) }
复制代码
由于Int.init(String)
是可失败构造器,maybeInts
中的元素类型是Int?。因为foo
不是整数,所以数组的最后一个元素是nil
。我们可以尝试在while let
循环中解封最外层的可选类型,看看其中包含的是不是nil
,如果不是的话就先解封,然后运行循环的主体部分:
var generator = mabeInts.generate()
while let maybeInt = generator.next() {
//maybeInt的类型是Int?
}
复制代码
当循环到最后一个元素——根据"foo"
字符串得到的nil
,时next()
方法返回的是.Some(nil)
。然后这个返回结果被解封,其中包含的的nil
与maybeInt
绑定。如果我们只想遍历数组中所有不为nil
的元素,在for
循环中可以用for case
匹配:
for case let i? in maybeInts {
//i是Int类型而不是Int?
}
复制代码
我们用到了i?
这种写法,这表示只匹配不为nil
的值。这是.Some(i)
的缩写,所以刚刚那个循环也可以这样写:
for case let .Some(i) in maybeInts {
}
复制代码
关于使用case
匹配的原理和更多用法,我的另一篇文章从原理分析Swift的switch怎么比较对象中有详细解释
未解封的可选类型的作用域
先来看一段代码:
if let firstElement = a.first {
//使用firstElement
}
//在if代码块外,你无法使用firstElement
复制代码
这里的firstElement
,只能在if
语句中使用。事实上这样不仅不会导致不方便,反而有好处。我们“不得不”解封并使用a.first
,这确保我们不会因为粗心而忘记相应的检查。
如果我们只关心可选绑定失败了会怎样,那么就可以使用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)
}
复制代码
注意,我们必须保证在else
的结尾,能跳出当前的作用域,一般可以通过return
或fatalError
实现。如果在循环中使用guard
,那么在else
中应该用break
或continue
。
我们可以注意到guard
和if
有些类似。有些时候用guard
比if
更复杂。不过guard
在阅读代码时也是一个醒目的标记,它表示“我在做一些检查,而且检查不通过就会退出”。编译器会确保你真的会在坚持不通过时退出当前代码块,否则就会产生编译错误。所以如果使用guard
和if
皆可,更推荐使用guard
。
可选链
在Objective-C中,向nil
对象发送消息其实是空指令。在Swift中,通过可选绑定可以达到同样的效果:
self.delegate?.callback()
复制代码
通过可选链调用的方法返回的结果也是可选类型。比如:
//假设我们有一个Int?类型的变量i,我们想要找到它的successor
let j = i?.successor()
复制代码
正如可选链的名字所示,它可以把多个操作串联起来:
let j = i?.successor().successor()
复制代码
不过这看上去有些奇怪。我们刚刚说过可选链的返回结果还是可选链,所以在第一个successor
后面为什么没有?
呢?
其实官方文档已经说的很清楚了,可选链是在每个可选类型的后面加上?
。这里的i
是可选类型,所以调用它的successor()
需要加?
,但successor()
函数本来的返回结果是Int,所以不用加?
。这里需要严格区分一个概念,即函数的返回类型和可选链的返回类型。比如successor()
函数返回结果是Int
但整个可选链返回的结果是Int?
,这是由于可选链有可能因为i
为nil
而失败,问题并不是出在successor()
函数上。
换句话说,如果successor()
函数自身的返回结果是可选类型的,那我们就需要在后面加上?
表示把这个可选链延续下去。
可选链表示一种可选类型的串联,所以在下标脚本和调用函数时也可以使用可选链。它不仅作为表达式的右值,还可以作为左值:
splitViewController?.delegate = myDelegate
复制代码
空合运算符
如果想在解封可选类型遇到nil
时,用一个默认值替换它,这时候可以使用空合运算符:
let stringInteger = "1"
let i = Int(stringInteger) ?? 0
复制代码
空合运算符和?:
三目运算符有些类似,事实上空合运算符总是可以用三目运算符重写,不过代码会比较复杂。我们可以在获取数组的第一个元素是,提供一个默认值:
let i = array.first ?? 0
复制代码
这样的代码更加简洁明了,一眼就能看出目的在于获取数组中第一个元素,添加在??
后面的0是默认值。此前的三元运算符,首先要检查,然后才是返回值,最后是默认值。这个检查的语法还很容易写反(不小心把默认值放在中间,真实值放在最后)。
空合运算符还可以形成一个链。如果我们有多个变量可能是可选类型,并希望选择第一个非可选类型的变量,可以这样写:
let i: Int? = nil
let j: Int? = nil
let k: Int? = 42
let n = i ?? j ?? k ?? 0
复制代码
空合运算符也可以不提供默认值,但这样最后的返回结果是可选类型的:
let m = i ?? j ?? k
// m 的类型是Int?
复制代码
遇到嵌套的可选类型,需要注意区别a ?? b ?? c
和(a ?? b) ?? c
。前者是空合运算符串联,得到的依然是可选类型,而后者首先解封内层,再解封外层:
let s1: String?? = nil
(s1 ?? "inner") ?? "outer" //值为inner
let s2: String?? = .Some(nil)
(s2 ?? "inner") ?? "outer" //值为outer
复制代码
可选类型map
我们经常会遇到这种情况:”接收一个可选类型,并且如果它不是nil
就对他进行某种变换“
这时就可以用可选类型的map
方法,它和数组的map
方法非常类似。它会接受一个闭包,表示了可选类型中的值的变换逻辑:
var i: Int? = 1
let j = i.map{ 2 * $0}
复制代码
如果i
为nil
,map
方法不做任何处理,所以j
也是nil
。像这里i
的值为1,map
方法就对i
中包装的元素(1)进行变换。这里的j
的值就是Optional(2)
。
可选类型的map
可以这样实现:
extension Optional {
func map<U>(transform: Wrapped -> U) -> U? {
if let value = self {
return transform(value)
}
return nil
}
}
复制代码
map
用途很多,比如如们想重载reduce
方法,它不接受初始值,而是直接把集合中的第一个元素当做初始值:
var array: Array<Int> = [1,2,3,4]
array.reduce(+)
复制代码
由于执行reduce
方法的数组可能为空,所以这个方法的返回值必须是可选类型(如果没有初始值,也就不会得到返回值,只能返回nil)。这就是本节开始我们提到的那个常用模式。所以可以使用map
方法来实现reduce
方法:
extension Array {
func reduce(combine: (Element, Element) -> Element) -> Element? {
return first.map {
self.dropFirst().reduce($0, combine: combine)
}
}
}
复制代码
可选类型flatMap
在可选类型的map
方法中,如果变换函数的返回值也是可选类型,那么返回结果就是嵌套的可选类型。比如获取数组的第一个字符串,并转化为整数:
let x = stringNumbers.first.map { Int($0) }
复制代码
因为map
返回的是可选类型,而Int(String)
返回的本来就是可选类型,所以x
的类型是Int??
。这时候我们可以用flatMap
方法把返回结果变成一个单层的可选类型:
let y = stringNumbers.first.flatMap { Int($0) }
复制代码
这时候的返回值y
就是Int?
类型的了。
或者也可以用if let
语句来写,因为可以根据先绑定的值计算出后绑定的值:
if let a = stringNumbers.first, b = Int(a) {
print(b)
}
复制代码
所以flatMap
可以这样实现:
extension Optional {
func flatMap<U>(transform: Wrapped -> U?) -> U? {
if let value = self, let transformed = transform(value) {
return transformed
}
return nil
}
}
复制代码
通过flatMap过滤nil
对于一个可选类型变量的集合,我们可能希望忽略掉其中的nil
。
考虑一个实际问题,有一个数组,里面有若各个字符串,我们希望求出所有字符串转化成数字之和的和。如果某个字符串不能转化成数字,就不考虑它。
实际上我们只需要一个可以过滤掉nil
的map
方法即可。标准库中sequence
重载的faltMap
方法就是这么做的,所以我们可以这样实现:
let numbers = ["1", "2", "3", "foo"]
numbers.flatMap { Int($0) }.reduce(0, combine: +) // 结果为6
复制代码
如果我们要自己实现这个flatMap
方法,我们先要定义一个flatten1
方法,过滤掉数组中所有的nil
,返回由所有不为nil
的可选类型构成的数组:
func flatten1<S: SequenceType, T where S.Generator.Element == T?>(source: S) -> [T] {
return Array(source.lazy.filter { $0 != nil}.map { $0! })
}
复制代码
这个方法没有拓展任何协议,因为我们要确保调用这个方法的Sequence
中的元素必须是可选类型,而协议拓展并不支持这一点。不过有了这个方法之后,我们要实现的flatMap
方法就很简单了:
extension SequenceType {
func flatMap<U>(transform: Generator.Element -> U?) -> [U] {
return flatten1(self.lazy.map(transform))
}
}
复制代码
在上面两个方法中,lazy
修饰符延迟真正的数组创建,可以避免为临时的中间数组分配内存空间。虽然这是一个比较小的优化,但如过数组很大,这么做还是值得的。
可选类型的相等
有时候我们不关注可选类型是否为nil
,而关注它是否含有某个确切的值:
if regex.characters.first == "^" {
//只匹配字符串的开始
}
复制代码
要想这么写,我们需要让可选类型实现==
运算符:
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
}
}
复制代码
不过根据重载的==
运算符,我们的代码应该这样写:
//要比较两个可选类型,其实不需要把"^"声明成可选类型。
if regex.characters.first == Optional("^") {
//只匹配字符串的开始
}
复制代码
之所以我们不用写.Some()
,是因为swift总是在需要的时候可以把非可选类型升级成可选类型以完成类型匹配。这种自动升级非常重要,比如我们实现可选类型的map
方法时,变换了可选类型内部的值并返回,但map
的返回结果是可选类型的,所以其实是编译器自动为我们做了这一步的转化。这样就不用把代码写成:Optional(transform(value))
了。
Swift的代码也依赖于这一特性。比如字典的下标脚本根据键来查找值,并返回它的可选类型。它的下标脚本既可以获取值,也可以赋值,如果没有隐式转化,赋值就要这样写:myDict["someKey"] = Optional(someValue)
。
基于键的下标脚本赋值,如果传入的值为nil
,那么这个键会被移除。这个特性很有用,但在用字典时,遇到可选类型也要小心:
var dictWithNils: [String: Int?] = [
"one": 1,
"two": 2,
"none": nil,
]
复制代码
这个字典有三个键,其中一个的值是nil
。如果我们想让"two"的值也是nil
,这样的代码是错误的:
dictWithNils["two"] = nil
复制代码
因为它实际上会把"two"
这个键移除。
如果我们想改变某个键的值,需要在下面的三种写法中任选一种自己觉得比较清晰的:
dictWithNils["two"] = Optional(nil)
dictWithNils["two"] = .Some(nil)
dictWithNils["two"]? = nil
复制代码
第三种写法和前两种的区别在于,它是基于可选链的,所以如果键不存在,它不会插入一条新的记录。
回到主题上来,尽管可选类型重载了==
运算符,但这并不表示他们就能实现Equatable
协议。根据可选类型重载的==
运算符可以看出,这需要保证任意一个它所包含的值的类型都实现了==
运算符。一旦可选类型实现了Equatable
协议,就可以在case
语句中进行匹配。
关于使用case
匹配的原理和更多用法,我的另一篇文章从原理分析Swift的switch怎么比较对象中有详细解释
比较可选类型
与==
运算符类似,我们还可以实现可选类型的<
运算符,这需要可选类型内部封装的值实现了Comparable
协议。nil
总是比任何非nil
的值小。这就意味着,nil
比任何负数都小。这一点在排序时需要重视:
let temps = ["-459.67", "98.6", "0", "warm"]
print(temps.sort { Double($0) < Double($1) })
复制代码
从运行结果中可以看出,warm
转换成数字是小于-459.67的
["warm", "-459.67", "0", "98.6"]
复制代码