Swift学习笔记-深入理解可选类型

? 和 ! 是什么

接触 Swift 的小伙伴们肯定对 ?! 这俩符号不陌生。他们广泛地出现在 iOS SDK 的各个地方。IDE 也会自动补全这两个家伙。但是这两个家伙到底是啥?

这两个符号表达了 Swift 的一个重要的特性,那就是可选类型(Optional Type)。可选类型是 Swift 引入的一个概念,它为那些在编译时不能确定是否有值的变量做了一个包装。

?

可选类型实际上是这样一个枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum Optional<Wrapped> : _Reflectable, NilLiteralConvertible {
case None
case Some(Wrapped)
/// Construct a `nil` instance.
public init()
/// Construct a non-`nil` instance that stores `some`.
public init(_ some: Wrapped)
/// If `self == nil`, returns `nil`. Otherwise, returns `f(self!)`.
@warn_unused_result
public func map<U>(@noescape f: (Wrapped) throws -> U) rethrows -> U?
/// Returns `nil` if `self` is nil, `f(self!)` otherwise.
@warn_unused_result
public func flatMap<U>(@noescape f: (Wrapped) throws -> U?) rethrows -> U?
/// Create an instance initialized with `nil`.
public init(nilLiteral: ())
}

由于这个枚举需要经常被使用,总是让大家写 var num: Optional<Int> 这样的代码,恐怕没几个人愿意。所以苹果的工程师把它包装成了 ? 给大家吃了个糖。

可见,Optional 枚举有两种状态,None 代表没有值,Some 代表有值。
基本类型的变量在使用之前必须被初始化,否则无法通过编译。可选类型可以不经过初始化就直接使用,可选类型会被初始化为 nil。

也就是说在 Swift 中,所有的非可选类型在使用时都是一定被初始化过的。这保证了非可选变量在使用的时候只有一种状态:有值。这为 Swift 程序提供了极强的安全性。

但是在实际应用中有些变量的值并不能在编译时确定下来,所以光有非可选类型是不够的,这时候就需要可选类型的支持了。可选类型的作用也就在于提供可空类型的变量。

对于可选值,我们并不能直接访问被它包装的值。这些值需要我们对可选值解包之后才能访问。

如以下代码:

1
2
3
var a:NSString? = "test"
print(a)
// output: Optional(test)

我们可以看到字符串 test 是被 Optional 泛型枚举包装着的,如果这是直接调用 NSString 的方法是不能编译通过的。

!

! 表示对可选类型的变量强制拆包,和 ? 一样,它也是一个语法糖,实际上它是这个枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public enum ImplicitlyUnwrappedOptional<Wrapped> : _Reflectable, NilLiteralConvertible {
case None
case Some(Wrapped)
/// Construct a `nil` instance.
public init()
/// Construct a non-`nil` instance that stores `some`.
public init(_ some: Wrapped)
/// Construct an instance from an explicitly unwrapped optional
/// (`Wrapped?`).
public init(_ v: Wrapped?)
/// Create an instance initialized with `nil`.
public init(nilLiteral: ())
/// If `self == nil`, returns `nil`. Otherwise, returns `f(self!)`.
@warn_unused_result
public func map<U>(@noescape f: (Wrapped) throws -> U) rethrows -> U!
/// Returns `nil` if `self` is nil, `f(self!)` otherwise.
@warn_unused_result
public func flatMap<U>(@noescape f: (Wrapped) throws -> U!) rethrows -> U!
}

我们还可以这样声明变量:

1
var label:UILabel!

这个枚举直译过来就是 隐式拆包 Optional,其实就是在你每次操作这种类型的值时候都会自动在操作前面补上一个 ! 拆包。

! 用于 Optional 类型变量的时候表示对变量进行强制拆包,而不管该变量又没有值。这样一来,当变量没有值的时候(nil)强制拆包将导致程序运行时崩溃。

可选类型的好处在于在编译时为语言提供了安全性,最大程度上避免了野指针调用使程序崩溃的情况发生。

调用可选类型变量的属性和方法

上面说到了,我们不能直接对可选类型变量调用方法和属性。这时我们需要对这个可选类型解包。
假设现在有 Person 和 Name 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Name {
var firstName: String = ""
var lastName: String = ""
init (firstName:String, lastName:String)
{
self.firstName = firstName
self.lastName = lastName
}
}

class Person {
var name:Name
init (name:Name)
{
self.name = name
}
}

强制解包

使用感叹号 ! 进行强制解包。顾名思义,强制解包就是不管变量是否有值都对其进行解包。有值的时候,解包成功,程序正常运行。如果没有值(等于 nil),解包会失败,程序在运行时发生崩溃。

1
2
3
4
5
6
7
8
9
// 下面代码正常运行
var p1: Person? = Person(name: Name(firstName: "Richard", lastName: "Lu"))
print(p1!.name.firstName)
//output: Richard

// 下面代码会发生错误
var p2: Person?
print(p2!.name.firstName)
// error: EXC_BAD_INSTRUCTION

强制解包是一种不安全的做法。很可能导致程序在运行时崩溃。所以不经判断直接解包的方式一般是不推荐使用的。除非用在那些你确定一定会有值的变量上,比如 IBOutlet 等。

改进一下,添加一些安全代码就可以了:

1
2
3
4
var p4: Person? = Person(name: Name(firstName: "Richard", lastName: "Lu"))
if p4 != nil {
print(p4!.name.firstName)
}

Optional Binding

在 if 语句中可以对可选类型变量进行隐式解包,同时判断可选类型是否有值,根据判断的结果进行进一步的操作。这也是推荐的一种做法。

1
2
3
4
5
var p3: Person? = Person(name: Name(firstName: "Richard", lastName: "Lu"))
if let person = p3 {
print(person.name.firstName)
}
// output: Richard

可选链(Optional Chaining)

上面的情况是比较简单的,毕竟只有最外层的对象是可选类型的。如果对象的属性也是可选类型呢?假设我们的 Person 和 Name 类变成了下面的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Name {
var firstName: String = ""
var lastName: String = ""
init (firstName:String, lastName:String)
{
self.firstName = firstName
self.lastName = lastName
}
}

class Person {
var name: Name?
init (name: Name)
{
self.name = name
}
}

我们现在想要取到 firstName 的值,使用 Optional Binding 或者强制拆包的方法要这么写:

Optional Binding

1
2
3
4
5
6
7
var p: Person? = Person(name: Name(firstName: "Richard", lastName: "Lu"))
if let person = p {
if let name = person.name {
print(name.firstName)
}

}

强制拆包

1
2
3
4
5
6
var p: Person? = Person(name: Name(firstName: "Richard", lastName: "Lu"))
if p != nil {
if p!.name != nil {
print(p!.name!.firstName)
}
}

可见不是一般的麻烦,一层套一层的判断逻辑搞的人头大。不过 Swift 早就为你设计了可选链( Optional Chaining )这种功能来实现方便的调用。

使用可选链,对于如上的调用我们只要这样写就够了:

1
2
3
4
5
var p: Person? = Person(name: Name(firstName: "Richard", lastName: "Lu"))
if let firstName = p?.name?.firstName
{
print(firstName)
}

对可选值直接进行链式操作,使得调用变得很方便。但是这里有个细节需要注意。可选链表达式中只要有一个 Optional 值,整个表达式的结果就都是 Optional 值,不管最后取到的值是不是 Optional 值,也不管表达式中有多少个 Optional 值。 所以对于可选链表达式的结果需要按照如上方法来取真实的值。如果我们直接打印 p?.name?.firstName 最终得到的将是一个被 Optional 包装的 firstName 值。