注册

Swift系列 -- 可选类型

4f4b988732c246b4a65da90aacb0cc0a~tplv-k3u1fbpfcp-zoom-crop-mark:1304:1304:1304:734.awebp?


前言

好记性不如烂笔头,学习过后还是要总结输出才能更有利于对知识的消化吸收。因此对于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显式指定了类型为Intb通过类型推断也被指定为Int,将String赋值给这两个变量时都报错Cannot assign value of type 'String' to type 'Int' 。

对于cstr两个变量,将其初始值赋值为nil,结果会报错nil无法为指定类型赋初始化值。需要注意的一点是,变量c虽然为Int类型,但是其初始值并不为0,即 var c:Int 和 var c:Int = 0 并不等价

不过在实际的开发过程中,我们经常会遇到无法确定一个变量是否有值的情况,比如在一个相机App中,当我们获取当前摄像头时,不能确定摄像头是否正在被其它App使用,或者摄像头硬件本身有什么问题,因此无法确定是否可以获取成功,那么此时我们就可能得到一个nil值。此时,就需要有一种类型可以接收nil,又可以接收正常的值。在Swift中,用以实现这种类型的就是可选类型

Swift的可选类型的定义方式为类型+?,具体代码如下:Xnip2021-11-17_10-57-25.png可选类型的变量可以给定一个对应类型的初始值,若不给定,则其默认值为nil

当可选类型有具体的值时,与其对应的类型也是有区别的,不能做等价处理,以Int为例:Xnip2021-11-17_14-14-51.pnga为可选类型的Int,b为普通的Int类型,虽然值都为20,但是a的类型打印出来是Optional(20)。在LLDB中po一下查看ab,结果如下:Xnip2021-11-17_14-32-30.png可以发现,b是一个纯粹的Int值20,而a是在20外面包了一层,这一点类似于一个盒子,如下图所示:Xnip2021-11-17_14-51-56.png

图中红色部分表示存储的值,如果可选类型中存储有值,则盒子中存储具体的值,本例中为Int值2,如果可选类型为nil,则盒子为空。

那么如果是多重可选项呢?即可选项能否包裹一层可选项呢?代码如下:

let result:Int?? = 20

通过LLDB调试,可以看到其实际结构如下所示:

Xnip2021-11-18_17-55-34.png

画图可表示为:

Xnip2021-11-18_17-52-31.png

通过LLDB打印出来可以看到可选类型是由一个Optional包裹的类型,那么Optional是什么呢?其实Optional是一个枚举类型,可以发现其定义如下所示:Xnip2021-11-18_23-34-47.png因此如下图所示的代码是等价的:Xnip2021-11-18_23-40-20.pngOptional通过泛型来指定其要包装的类型,并且Optional遵守了ExpressibleByNilLiteral协议,遵守该协议的枚举、结构体或类初始化时允许值为nil。

Optional枚举内部包含nonesome两个case,如果值为nil,则属于none,有值的话则包装为some,由此也可看出Swift枚举的强大。

二、强制解包

既然可选类型是将对应类型的值包在一个盒子中,那么是否可以将可选类型的值赋值给对应类型的变量呢?可以简单做个测试,结果如下:Xnip2021-11-17_17-02-06.png答案显然是否定的,编译器在编译时就会报错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后的{}。

如果有多个可选项绑定,中间需要用,隔开,而不能使用&&,如图所示:

Xnip2021-11-17_18-49-11.png

Xnip2021-11-17_18-51-01.png

四、空合并运算符

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或者抛出异常,否则编译器会报错如下:Xnip2021-11-18_15-51-34.png

在最初接触到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枚举,其包含nonesome两个case,none表示当前变量值为nil,some表示当前变量值不为nil
  • Swift可选项不能直接赋值给其包装的类型所对应的变量,应该在解包后赋值,但是需要注意的是,要在保证有值的情况下强制解包,否则会Crash
  • 对于可选类型的判空处理,可以使用可选项绑定来做,这样更加优雅

与OC不同,Swift更加注重安全性,尤其是对于nil的处理上,Swift更加的严谨,虽然在刚接触时会有些不适应,但是在开发过程中却可以省去很多对nil的容错处理,由此也可以看出Swift的强大与设计精妙。以上即为对于Swift中的可选类型的总结,欢迎大家指正。

0 个评论

要回复文章请先登录注册