隐式解包Optional
相对于普通的 Optional 值,在 Swift 中我们还有一种特殊的 Optional,在对它的成员或者方法进行访问时,编译器会帮助我们自动进行解包,这就是 ImplicitlyUnwrappedOptional
。在声明的时候,我们可以通过在类型后加上一个感叹号 ( !
) 这个语法糖来告诉编译器我们需要一个可以隐式解包的 Optional 值:
var maybeObject: MyClass!
首先需要明确的是,隐式解包的 Optional 本质上与普通的 Optional 值并没有任何不同,只是我们在对这类变量的成员或方法进行访问的时候,编译器会自动为我们在后面插入解包符号 !
,也就是说,对于一个隐式解包的下面的两种写法是等效的:
var maybeObject: MyClass! = MyClass()
maybeObject!.foo()
maybeObject.foo()
我们知道,如果 maybeObject
是 nil
的话那么这两种不加检查的写法的调用都会导致程序崩溃。 而如果 maybeObject
是普通的 Optional 的话,我们就只能使用第一种显式地加感叹号的写法,这能提醒我们也许应该使用 if let
的 Optional Binding
的形式来处理。而对隐式解包来说,后一种写法看起来就好像我们操作的 maybeObject
确实是 MyClass
类的实例,不需要对其检查就可以使用 (当然实际上这不是真的)。使用隐式解包 Optional 的最大好处是对于那些我们能确认的 API 来 说,我们可直接进行属性访问和方法调用,会很方便。但是需要牢记在心的是,隐式解包不意味着 “这个变量不会是nil
,你可以放心使用” 这种暗示,只能说 Swift 通过这个特性给了我们一种简便但是危险的使用方式罢了。
另外,其实在 Apple 的不断修改下,在 Swift 的正式版本中,已经没有太多的隐式解包的 API 了。现在比较常见的隐式解包的 Optional 就只有使用 Interface Builder 时建立的 IBOutlet
了:
@IBOutlet weak var button: UIButton!
如果没有连接 IB 的话,对 button
的直接访问会导致应用崩溃,这种情况和错误在调试应用时是很容易被发现的问题。在我们的代码的其他部分,还是少用这样的隐式解包的 Optional 为好,很多时候多写一个Optional Binding
就可以规避掉不少应用崩溃的风险。
多重 Optional
我们使用的类型后加上 ?
的语法只不过是 Optional
类型的语法糖,而实际上这个类型是一个 enum
:
public enum Optional<Wrapped> : ExpressibleByNilLiteral {
case none
case some(Wrapped)
//...
}
在这个定义中,对 Wrapped
没有任何限制,也就是说,我们是可以在 Optional
中装入任意东西的,甚至也包括 Optional
对象自身。打个形象的比方,如果我们把 Optional
比作一个盒子,实际具体的 String
或者 Int
这样的值比作糖果的话,当我们打开一个盒子 (unwrap) 时,可能的结果会有三个 -- 空气,糖果,或者另一个盒子。
对于盒子中的盒子,有时候使用时就相当容易出错。 特别是在和各种字面量转换混用的时候需要特别注意。
对于下面这种形式的写法:
let string: String? = "string"
let anotherString: String?? = string
print(anotherString) //Optional(Optional("string"))
我们可以很明白地知道 anotherString
是 Optional<Optional<String>>
。但是除开将一个 Optional
值赋给多重 Optional
以外,我们也可以将直接的字面量值赋给它:
let literalOptional: String?? = "string"
print(literalOptional) //Optional(Optional("string"))
这种情况还好,根据类型推断我们只能将 Optional<String>
放入到 literalOptional
中,所以可以猜测它与上面提到的 anotherString
是等效的。但是如果我们是将 nil
赋值给它的话,情况就有所不同了。考虑下面的代码:
var aNil: String? = nil
var anotherNil: String?? = aNil
var literalNil: String?? = nil
print(anotherNil) // Optional(nil)
print(literalNil) // nil
anotherNil
和 literalNil
是不是等效的呢?答案是否定的。 anotherNil
是盒子中包了一个盒子, 打开内层盒子的时候我们会发现空气; 但是 literalNil
是盒子中直接是空气。使用中一个最显著的区别在于:
if let a = anotherNil {
print("anotherNil")
}
if let b = literalNil {
print("literalNil")
}
这样的代码只能输出 anotherNil
。
另一个值得注意的地方时在Playground 中运行时,或者在用 lldb 进行调试时,直接使用 po
指令打印 Optional 值的话,为了看起来方便,lldb 会将要打印的 Optional 进行展开。如果我们直接打印上面的 anotherNil
和 literalNil
,得到的结果都是 `nil`:
(lldb) po anotherNil
▿ Optional<Optional<String>>
- some : nil
(lldb) po literalNil
nil
如果我们遇到了多重 Optional 的麻烦的时候,这显然对我们是没有太大帮助的。我们可以使用 fr v -R
命令来打印出变量的未加工过时的信息,就像这样:
(lldb) fr v -R anotherNil
(Swift.Optional<Swift.Optional<Swift.String>>)
anotherNil = some {
// ...
}
(lldb) fr v -R literalNil
(Swift.Optional<Swift.Optional<Swift.String>>)
literalNil = none {
// ...
}
这样我们就能清晰地分辨出两者的区别了。
Optional Map
我们经常会对 Array
类型使用 map
方法,这个方法能对数组中的所有元素应用某个规则,然后返回一个新的数组。我们可以找到这个方法的定义:
public func map<T>(_ transform: (Element) throws -> T) rethrows -> [T]
举个简单的例子:
let arr = [1,2,3]
let doubled = arr.map {
$0 * 2
}
假设我们有个需求,要将某个 Int?
乘 2。一个合理的策略是如果这个 Int?
有值的话,就取出值进行乘 2 的操作,如果是 nil
的话就直接将 nil
赋给结果。依照这个策略,我们可以写出如下代码:
let num: Int? = 3
var result: Int?
if let realNum = num {
result = realNum * 2
} else {
result = nil
}
print(result) // Optional(6)
其实我们有更优雅简洁的方式,那就是使用 Optional 的 map
。对的,不仅在 Array
或者说 CollectionType
里可以用 map
,如果我们仔细看过 Optional
的声明的话,会发现它也有一个 map
方法:
// if `self == nil`, returns `nil`.Otherwise, returns `transform(self!)`.
public func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
这个方法能让我们很方便地对一个 Optional 值做变化和操作,而不必进行手动的解包工作。输入会被自动用类似 Optinal Binding 的方式进行判断,如果有值,则进入 transform
的闭包进行变换,并返回一个U?
;如果输入就是 nil
的话,则直接返回值为 nil
的 U?
。
有了这个方法,上面的代码就可以大大简化,而且 result
甚至可以使用常量值:
let result = num.map{
$0 * 2
}
print(result) // Optional(6)
如果您了解过一些函数式编程的概念,可能会知道这正符合函子 (Functor) 的概念。不论是 Array
还是 Optional
,它们拥有一个同样名称的 map
函数并不是命名上的偶然。函子指的是可以被某个函数作用,并映射为另一组结果,而这组结果也是函子的值。在我们的例子里,Array
的map
和 Optional
的map
都满足这个概念,它们分别将 [self.generator.Element]
映射为 [T]
以及 Wrapped?
映射为 U?
。Swift 是一門非常适合用函数式编程的思想来进行程序设计的语言,如果您想多了解一 些函数式编程的思想,ObjC 中国的《函数式 Swift》会是入門 Swift 函数式编程的好选择。
Protocol Extension
在Swift 2中,我们可以对一个已有的protocol进行扩展,而扩展中实现的方法将作为实现扩展的类型的默认实现。
在具体的实现这个协议的类型中,即使我们什么都不写,也可以编译通过。进行调用的话,会直接使用 extension
中的实现。当然,如果我们需要在类型中进行其他实现的话,可以像以前那样在具体类型中添加这个方法。也就是说,protocol extension 为 protocol 中定义的方法提供了一个默认的实现。
有了这个特性以 后,之前被放在全局环境中的接受 CollectiontType
的 map
方法,就可以被移动到 CollectiontType
的协议扩展中去了:
extension CollectionType {
public func map<T>(@noescape transform: (self.Generator.Element) -> T) -> [T]
//...
}
另一个用到 protocol extension
的地方是 optional
的协议方法。通过提供 protocol
的 extension
,我们为 protocol
提供了默认实现,这相当于变相将 protocol
中的方法设定 为了 optional
。
对于 protocol extension
来说,有一种会非常让人迷惑的情况,就是在协议的扩展中实现了协议里没有定义的方法时的情况。
protocol A2 {
func method1() -> String
}
extension A2 {
func method1() -> String {
return "hi"
}
func method2() -> String {
return "hi"
}
}
struct B2: A2 {
func method1() -> String {
return "Hello"
}
func method2() -> String {
return "Hello"
}
}
let b2 = B2()
print(b2.method1())//Hello
print(b2.method2())//Hello
let a2 = b2 as A2
print(a2.method1())//Hello
print(a2.method2())//Hi
a2
和 b2
是同一个对象,只不过我们通过 as
告诉编译器我们在这里需要的类型是 A2
。但是这时候在这个同样的对象上调用同样的方法调用却得到了不同的结果,发生了什么?
我们可以看到,对 a2
调用 method2
实际上是协议扩展中的方法被调用了,而不是 a2
实例中的方法被调用。我们不妨这样来理解:对于 method1
,因为它在 protocol
中被定义了,因此对于一个被声明为遵守协议的类型的实例 (也就是对于 a2
) 来说,可以确定实例必然实现了method1
, 我们可以放心大胆地用动态派发的方式使用最终的实现 (不论它是在类型中的具体实现,还是在协议扩展中的默认实现); 但是对于method2
来说,我们只是在协议扩展中进行了定义,没有任何规定说它必须在最终的类型中被实现。在使用时,因为 a2
只是一个符合 A2
协议的实例,编译器对 method2
唯一能确定的只是在协议扩展中有一个默认实现,因此在调用时,无法确定安全,也就不会去进行动态派发,而是转而编译期间就确定的默认实现。
总结:
如果类型推断得到的是实际类型
- 那么类型中的实现将被调用;如果类型中没有实现的话,那么协议扩展中的默认实现将被调用(类似协议中的可选方法)
如果类型推断得到的是协议,而不是实际类型
- 并且方法在协议中进行了定义,那么类型中的实现将被调用;如果类型中没有实现,那么协议扩展中的默认实现将被调用(类似协议中的可选方法)
否则(也就是方法没有再协议中定义),扩展中的默认实现将被调用。