Swift系列 -- 可选类型
- 「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」
前言
好记性不如烂笔头,学习过后还是要总结输出才能更有利于对知识的消化吸收。因此对于Swift
的学习作了一个系列总结:
本篇作为Swift学习总结的第二篇文章,主要探索的是关于Swift可选项
的内容。
还记得在使用OC开发时,对于一个对象而没有初始化时,其默认值为nil
,并且可以在后续的操作中将该对象重新赋值为nil
。因此我们经常会遇到因为对象为nil
造成的程序错误,例如向数组中插入了nil
造成闪退。
但是Swift是一门类型安全语言
,Swift的类型是不允许变量值为nil
的,而不论是引用类型,还是值类型。但是,在实际的开发中,确实存在变量可能为nil
的情况,因此Swfit中还提供了一种特殊的类型 -- 可选类型
,用于处理这种情况,下面就一起探索下可选项
的相关内容。
一、可选类型的本质
Swift的类型安全是指,定义一个变量时,给定了类型,那么就不能再将其它类型的值赋给该变量。当然,这也不是说Swift中定义变量时,必须显式的指定变量类型。Swift还有类型推断,即根据给定的值,自动确定变量类型。如下代码所示:
let a:Int
a = "123" => Cannot assign value of type 'String' to type 'Int'
var b = 20 // 赋值为20,类型推断为Int
b = "swift" => Cannot assign value of type 'String' to type 'Int'
var c:Int = nil => 'nil' cannot initialize specified type 'Int'
var str: String = nil => 'nil' cannot initialize specified type 'String'
在上述事例代码中,a显式指定了类型为Int
,b通过类型推断也被指定为Int
,将String
赋值给这两个变量时都报错Cannot assign value of type 'String' to type 'Int'
。
对于c和str两个变量,将其初始值赋值为nil,结果会报错nil
无法为指定类型赋初始化值。需要注意的一点是,变量c
虽然为Int
类型,但是其初始值并不为0,即 var c:Int 和 var c:Int = 0 并不等价
。
不过在实际的开发过程中,我们经常会遇到无法确定一个变量是否有值的情况,比如在一个相机App中,当我们获取当前摄像头时,不能确定摄像头是否正在被其它App使用,或者摄像头硬件本身有什么问题,因此无法确定是否可以获取成功,那么此时我们就可能得到一个nil
值。此时,就需要有一种类型可以接收nil
,又可以接收正常的值。在Swift中,用以实现这种类型的就是可选类型
。
Swift的可选类型的定义方式为类型+?,具体代码如下:可选类型的变量可以给定一个对应类型的初始值,若不给定,则其默认值为nil
。
当可选类型有具体的值时,与其对应的类型也是有区别的,不能做等价处理,以Int为例:a
为可选类型的Int,b
为普通的Int类型,虽然值都为20,但是a的类型打印出来是Optional(20)。在LLDB中po一下查看a
和b
,结果如下:可以发现,b
是一个纯粹的Int值20,而a
是在20
外面包了一层,这一点类似于一个盒子,如下图所示:
图中红色部分表示存储的值,如果可选类型中存储有值,则盒子中存储具体的值,本例中为Int值2
,如果可选类型为nil
,则盒子为空。
那么如果是多重可选项呢?即可选项能否包裹一层可选项呢?代码如下:
let result:Int?? = 20
通过LLDB调试,可以看到其实际结构如下所示:
画图可表示为:
通过LLDB打印出来可以看到可选类型是由一个Optional
包裹的类型,那么Optional
是什么呢?其实Optional
是一个枚举类型,可以发现其定义如下所示:因此如下图所示的代码是等价的:Optional通过泛型来指定其要包装的类型,并且Optional遵守了ExpressibleByNilLiteral
协议,遵守该协议的枚举、结构体或类初始化时允许值为nil。
Optional枚举内部包含none
和some
两个case,如果值为nil,则属于none,有值的话则包装为some,由此也可看出Swift枚举的强大。
二、强制解包
既然可选类型是将对应类型的值包在一个盒子中,那么是否可以将可选类型的值赋值给对应类型的变量呢?可以简单做个测试,结果如下:答案显然是否定的,编译器在编译时就会报错Value of optional type 'Int?' must be unwrapped to a value of type 'Int'
,Int?必须解包成一个 Int类型。
Swift中可选类型的强制解包使用一个!
即可,代码如下所示:
let a:Int? = 20
var b:Int = 20
b = a!
代码第三行 b = a!
中,可选类型Int a
即解包为了Int
,并赋值给b
。当然a
依然是一个可选类型,其值依然为20。
强制解包需要注意以下几点:
- 1、强制解包后,对于原可选变量的值没有影响,其依然为可选类型
- 2、值为nil的可选类型,强制解包会发生闪退,因此在使用强制解包时,需要确定可选类型中的值不为
nil
与强制解包一起的还有一种类型,隐式解包的可选项,代码表现为类型 + !
。例子如下:
let result:Int! = 20
let realInt:Int = result
可以发现result可以直接赋值给一个Int的realInt
,因为隐式解包的可选项会隐式的将变量解包,而不会有明显的感知。不过需要明确的一点是,隐式解包依然是可选项,如果不是确定变量会一直有值,使用需要谨慎。
三、可选项绑定
使用可选项强制解包时,为例防止值为nil
的闪退,我们可能会像下面这样写代码:
let a:Int? = 20
var b:Int
if a != nil {
b = a!
}
相对于直接解包,这样写安全性确实提高了一些,不过Swift提供了一种更加优雅的方式来解决这一问题,即可选项绑定,代码如下:
let a:Int? = 20
var b:Int
if let value = a {
b = value
} else {
print("a的值为nil")
}
如同代码中if后面的条件所示,可选项绑定的语法是let value = 可选项变量
。可选项绑定使用在条件判断等地方,如果可选项变量为nil,则条件为false
,如果可选项变量不为nil
,则会自动解包并赋值给value
,只是value的作用域仅限if条件后的{}
,不能用在else后的{}。
如果有多个可选项绑定,中间需要用,
隔开,而不能使用&&
,如图所示:
四、空合并运算符
Swift中还提供了空运算符 ??,其定义为如下代码
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T?
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T
空合并运算符是一个二元运算符,假定有两个变量 a 和 b,使用空合并运算符方式为 a ?? b
,使用时有以下注意事项:
- a??b,如果a为nil,则返回b,否则返回a自身
- a需要为可选项,否则虽然编译器不会报错,但是没有意义
- b可以是可选项,也可以不是可选项
- 不管a、b是否都是可选项,两者存储的类型要对应,例如Int?<=> Int 或 Int?<=> Int?
- 如果b不是可选项,a的值不为nil,则在返回 a 时,会自动解包,事实上b决定了返回值是否解包
4.1 空合并运算符使用举例
以下为空合并运算符的几个例子,假定a、b存储皆为Int
值:
a为nil,b为Int值2
let a:Int? = nil
let b:Int = 2
let result = a ?? b // result为b的值,且为Int类型
a为nil,b为可选类型Int值2
let a:Int? = nil
let b:Int? = 2
let result = a ?? b // result为b的值 Optional(2)
a不为nil,b为Int类型
let a:Int? = 3
let b:Int = 2
let result = a ?? b // result为a的值,并且已经解包为3
a不为nil,b为可选Int类型
let a:Int? = 3
let b:Int? = 2
let result = a ?? b // result为a的值,并且依然为Optional(3)
还有多个空合并运算符连接使用的情况,如下代码:
let a:Int? = 2
let b:Int? = nil
let c:Int = 4
let result = a ?? b ?? c // result值为2,是 a 解包后的值
如例所示,当多个??
连接使用时,决定result值的依然是最后一个变量c
,前面 a??b
得到了可选Int?值2
,因为c为int类型,所以得到解包后的Int值2
。
4.2 空合并运算符与可选项绑定
??
还可以与可选项绑定结合在一起使用,如下代码所示:
- 类似于
a != nil || b != nil
let a:Int? = 2
let b:Int? = nil
if let result = a ?? b { // 只要a和b中有一个不为空,就可以进入该条件判断
let c = result
print(c)
}
- 类似于
a != nil && b != nil
let a:Int? = 2
let b:Int? = nil
if let c = a, let d = b { // 只有a和b都不为nil时,才会进入条件判断
print(c)
print(d)
}
通过上述两种方式,可以更加精简的进行多个可选项的nil
值判断,并且在条件为真的情况下可以自动解包,直接使用解包后的值。
五、guard语句
guard语句与if语句类似,都是条件判断语句,其语法规则为:
guard 条件 else {
// 执行代码
}
不过与if语句
不同的是,guard
语句是条件为false
时,进入{}
执行代码。如下面的例子所示:
let a:Int? = 20
guard a != nil else {
print("a的值为nil")
return
}
print("a的值为\(a!)")
并且guard语句的代码块中,必须有return或者抛出异常
,否则编译器会报错如下:
在最初接触到guard语句时,可能想已经有了if语句,为什么还需要guard呢?并且guard实现的功能,if也可以实现,会觉得其有些多余。但是经过一段时间开发,对两者做出对比后,可以发现在一定程度上,guard表达的语义更加明确,代码的可读性更高。例如,上面的代码改成if语句如下:
if a == nil {
print("a的值为nil")
return
}
print("a的值为\(a!)")
对比两段代码,语义上我们符合我们预期的值 a != nil
,如果使用if语句是要判断a==nil,使用guard就判断a != nil
,不符合就return
即可,因此guard更加适合做容错判断。
六、总结
- Swift可选类型的本质是
Optional
枚举,其包含none
和some
两个case,none表示当前变量值为nil,some表示当前变量值不为nil - Swift可选项不能直接赋值给其包装的类型所对应的变量,应该在解包后赋值,但是需要注意的是,要在保证有值的情况下强制解包,否则会Crash
- 对于可选类型的判空处理,可以使用可选项绑定来做,这样更加优雅
与OC不同,Swift更加注重安全性,尤其是对于nil的处理上,Swift更加的严谨,虽然在刚接触时会有些不适应,但是在开发过程中却可以省去很多对nil的容错处理,由此也可以看出Swift的强大与设计精妙。以上即为对于Swift中的可选类型的总结,欢迎大家指正。