第四章——可选类型技术之旅

本文深入探讨Swift中的可选类型,包括可选绑定、可选链、空合运算符及可选类型的map和flatMap方法。并通过实例展示了如何有效利用这些特性进行编程。

本文系阅读阅读原章节后总结概括得出。由于需要我进行一定的概括提炼,如有不当之处欢迎读者斧正。如果你对内容有任何疑问,欢迎共同交流讨论。

可选绑定

我们可以利用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)。然后这个返回结果被解封,其中包含的的nilmaybeInt绑定。如果我们只想遍历数组中所有不为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的结尾,能跳出当前的作用域,一般可以通过returnfatalError实现。如果在循环中使用guard,那么在else中应该用breakcontinue

我们可以注意到guardif有些类似。有些时候用guardif更复杂。不过guard在阅读代码时也是一个醒目的标记,它表示“我在做一些检查,而且检查不通过就会退出”。编译器会确保你真的会在坚持不通过时退出当前代码块,否则就会产生编译错误。所以如果使用guardif皆可,更推荐使用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?,这是由于可选链有可能因为inil而失败,问题并不是出在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}
复制代码

如果inil,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

考虑一个实际问题,有一个数组,里面有若各个字符串,我们希望求出所有字符串转化成数字之和的和。如果某个字符串不能转化成数字,就不考虑它。

实际上我们只需要一个可以过滤掉nilmap方法即可。标准库中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"]
复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值