0%

详解可选型(Optional)

通常在其他语言中,nil 的本质就是 0,用 0 代表没有;但是 Swift 认为,0 应该是和其他整数一样的呀?程序员怎么知道 0 就是没有而不是 0 本身呢?于是,在 Swift 中出现了可选型的概念。在 Swift 中,nil 代表空,代表没有,nil 就是 nil,不再是一个整数 0 了。

可选型的基本使用

可选型写法很简单,普通类型后面加 ? 就可以了;通常一个变量如果不应该为空,一定要存在,那么就不应该设置为可选型,比如身份证号,每个人都一定有,那么就不要设置成可选型

1
let id: Int

如果这个变量可以为空,比如错误信息,错误信息可能有也可能没有,那么就适合设置成可选型

1
var errorMessage: String?

可选型的使用必须经过一个步骤就是解包(Unwrapp),可选型是无法直接使用的

最简单的解包方式就是强制解包,含义为程序员保证这个可选型变量/常量一定不会空;而如果其真的为空了就会崩溃,因此这是有风险的,程序员必须能保证才行。

1
2
3
4
5
6
var serverResponseCode: Int? = 404

// serverResponseCode = nil // 如果加上这句代码,强制解包失败就会崩溃
// 另外,只有可选型可以设置成 nil,非可选型是无法设置为 nil 的

let code = serverResponseC! // 此时 code 为 Int 类型

通常情况下,比如网络请求得到的数据,我们无法保证其一定不会 nil,此时就应该通过判断来非暴力的解包,我们通常能想到的应该是下面这样

1
2
3
if serverResponseCode != nil {
let code = serverResponseCode!
}

事实上,因为可选型解包是一个常用操作,Swift 为我们专门提供了简化语法,下面的语法和上面的完全等价

1
2
3
if let severResponseCode = serverResponseCode {
// 此时,大括号内的 severResponseCode 就变成了一个 Int 类型,可以直接使用
}

如果有多个可选型同时需要解包,那么它们可以并列写在同一个 if 语句里

1
2
3
4
5
6
var serverResponseCode: Int? = 404
var serverResponseMessage: String? = "not found"

if let severResponseCode = serverResponseCode, let serverResponseMessage = serverResponseMessage {
// 这个作用域内,解包后的变量可以直接使用
}

当然,可以使用 if 的一般都可以使用 guard,本文就不涉及 guard 的使用了。

可见可选型的基本使用并不难,可为空就设置成可选型,用的时候就需要解包,这样就可以了。那么,Swfit 为什么要给程序员添加这个麻烦呢?当然是为了安全啦,可以这么说,只要程序员合理使用可选型,原来在 Objective-C 中的野指针访问的问题基本就可以避免啦。Swift 只是强制程序员去主动思考了而已,原来是被动的,现在是把这个过程提前到代码编写阶段,不得不去考虑。

可选型的实质

可选型事实上是一个枚举,String? 实际上是 Optional<String>,它只是一个语法糖。下面两句代码是完全等价的

1
2
var optionalString: String? = "Hello"
var optionalString: Optional<String> = Optional.some("Hello")

而通常我们的解包

1
2
3
4
5
if let aString = optionalString {
aString // 解包
} else {
print("nil")
}

其实也可以看作下面的过程

1
2
3
4
5
6
switch optionalString {
case let .some(alString):
alString // 这里就是解包后的 String 类型变量
case .none:
print("none") // 这就是解包失败
}

我们可以尝试自己写一个这样的枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
enum MyOptional<T> {
case some(T)
case none
}

let optionaString = MyOptional.some("Hello") // 这里类型是通过类型推断得到的,当然也可以显式声明其类型 MyOptional<String>

switch optionaString {
case let .some(aString):
print(aString) // 这里就相当于解包成功咯
case .none:
print("none") // 这里就是解包失败,也就是 nil
}

Optional Chaining

我们知道可选型无法直接使用,那么我们就要先解包,然后使用,如果调用一个方法后返回类型还是可选型呢?那么我们再解包,再使用。那这样也太麻烦了吧?

所以呢,Swift 的这样一个语法就是解决这样的问题的,那就是 Optional Chaining,它的语法大约是下面这样的

1
2
let aString: String? = "Hello world"
let des = aString?.first?.description

一个可选型可以直接调用它对应的非可选型的方法,只不过返回值同样变成了一个可选型而已(即使这个方法的返回类型是一个非可选型),我们知道 description 的返回类型是 String 类型,可是此时 des 的类型是 String?

隐式可选型

在 UIKit 中,有很多类型是如 UIView! 这样的,比如 UIViewController 中的 view 属性,这就是隐式可选型。

隐式可选型是什么呢?其实也是一个可选型,也就是说 Int!Int? 都是可选型,都可以为 nil;那么区别在哪呢?区别很小,就是 Int! 可以省去解包这个步骤直接使用

1
2
let num: Int! = 3
let sum = num + 4

所以很简单,Int! 就是把强制解包的步骤给提前做啦,其他跟一般的可选型没啥区别;当然,这样肯定是有风险的,因为是强制解包嘛。所以程序员声明隐式可选型闭包保证一点,就是在真正使用它的时候,其必须不为 nil。

那隐式可选型有什么作用呢?目前来讲,其应用场景基本就是在定义类的时候,类的某些属性我们可以定义成隐式可选型。下面这个例子来自慕课网刘宇波老师的案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class City {

let cityName: String
unowned var country: Country

init(cityName: String, country: Country) {
self.cityName = cityName
self.country = country
}
}

class Country {

let countryName: String
// 这里如果不使用可选型,因为类的所有成员变量还没有被全部赋值,下面就无法在构造函数中使用 self,但是程序员又可以保证这个类的对象一旦创建,这个属性一定不为nil,所以这里使用了隐式可选型
var capitalCity: City!

init(countryName: String, capitalCity: String) {
self.countryName = countryName
self.capitalCity = City(cityName: capitalCity, country: self)
}
}

这个例子中, Country 类中的 capitalCity 属性被定义成了隐式可选型。

首先,为什么不能定义成一般的可选型呢?因为一个国家的首都都是确定存在的不可能为空啊,程序员其实不想把它定义成可选型,更不希望用户使用的时候还要解包。

那么就定义成非可选型呗?也不行,因为在构造函数里,我们在给 self.capitalCity 赋值的时候使用了 self,我们知道一个类的对象在构造完成前(因为 self.capitalCity 属性没有被赋值嘛)是不可以被使用的,那这怎么办呢?既然要用这个对象就要赋值,既然要赋值就要用这个对象,死循环了。

所以解决办法就是在赋值之前就让这个对象先构造完成,也就是把这个属性改成可选型,这样可选型可以不用初始化(默认为 nil),之后呢我们马上再给这个对象的这个属性赋上值,最终在用户使用这个对象的时候,这个属性可以保证不为 nil,同时用户也不用解包即可直接使用了。