一. 可选项的基本使用
可选项,一般也叫可选类型,它允许将值设置为nil。
在类型名称后面加个问号 ? 来定义一个可选项。
二. 强制解包(Forced Unwrapping)
可选项是对其他类型的一层包装,可以将它理解为一个盒子
如果为nil,那么它是个空盒子
如果不为nil,那么盒子里装的是:被包装类型的数据
如果要从可选项中取出被包装的数据(将盒子里装的东西取出来),需要使用感叹号 ! 进行强制解包
上面,强制解包以后Int?类型变成了Int类型。
如果对值为nil的可选项(空盒子)进行强制解包,将会产生运行时错误
三. 可选项绑定(Optional Binding)
- 一般的判断方法
判断可选项是否包含值
- 可选项绑定
但是每次都这样判断太麻烦,我们可以使用可选项绑定来判断可选项是否包含值,如果包含就自动解包,把值赋给一个临时的常量(let)或者变量(var),并返回true,否则返回false。
- 下面两种写法等价
如果条件判断中有可选项绑定就不能用&&,只能多个条件之间用逗号隔开。
- while循环中使用可选项绑定
四. 空合并运算符 ??(Nil-Coalescing Operator)
① 单个??
a ?? b
- a 是可选项,b 是可选项 或者 不是可选项
- b 跟 a 的存储类型必须相同 (比如a是Int?,b就必须是Int?或者Int)
- 如果 a 不为nil,就返回 a,如果 b 不是可选项,返回 a 时会自动解包
- 如果 a 为nil,就返回 b
根据上面的几条规则,可以发现:优先返回a,返回值类型⼀定和b类型保持⼀致。
想要知道为什么返回值类型⼀定和b类型保持⼀致,我们可以查看Swift对 ?? 的定义:
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T?
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T
可以发现:
① 第一个参数必须是可选项,第二个参数可以是可选项也可以不是可选项
② 但是返回值类型总是和第二个参数的类型保持一致。
注意:当可选项只有一个?的时候上面的结论没有任何问题,当有不止一个?的时候就不太一样了,这时候可以参考??的源码实现,如:iOS-Swift-标准库源码分析+项目实战
② 多个??一起使用
③ ??跟if let配合使用
五. guard语句
- 使用if语句进行登录判断
如果我们用if语句实现登陆功能,代码如下:
运行代码:
- guard语句
当guard语句的条件为true时,就会跳过guard语句,往后执行
当guard语句的条件为false时,就会执行大括号里面的代码
guard语句特别适合用来“提前退出”。
可以发现:guard语句只有条件成立的时候就继续往下走,条件不成立就执行else{}里面的内容,{}里面一般就是return。
- 使用guard语句进行登录判断
所以上面的if登录判断我们可以修改为使用guard语句进行登录判断,这样就简单明了多了。
当使用guard语句进行可选项绑定时,绑定的常量(let)、变量(var)也能在外层作用域中使用,如下:
六. 隐式解包(Implicitly Unwrapped Optional)
在某些情况下,可选项一旦被设定值之后,就会一直拥有值
在这种情况下,可以去掉检查,也不必每次访问的时候都进行解包,因为它能确定每次访问的时候都有值
可以在类型后面加个感叹号 ! 定义一个隐式解包的可选项
首先要明白,无论是Int?还是Int!都是可选类型,所以可以使用!进行强制解包,也可以通过打印验证,如下:
let num1 : Int! = 10
let num2 : Int = num1!
print(num1,num2)
//Optional(10) 10
上面代码,由于定义num1的时候定义的是可选项,所以在使用num1的时候可以用!解包,然后把它的值赋值给num2。又因为定义num1的时候就使用了隐式解包可选项,它会自动解包,所以在使用num1的时候其实可以省略!,所以上面的代码也可以下面这样写:
let num1 : Int! = 10
let num2 : Int = num1
print(num1,num2)
//Optional(10) 10
这样代码更简洁,以后使用num1的时候就不用加!了,如下:
由于隐式解包会偷偷的解包,所以如果刚开始的值为nil,那么偷偷解包就会报错,如下:
let num1 : Int! = nil
let num2 : Int = num1 //这里会偷偷的解包
然后报错:Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value
可能你会想,如果能保证一直有值,那么使用 var num1 : Int = 10 不是更好嘛!(其实的确也是的)
其实开发中尽量不要使用Int!,那你可能会说Int!不是没啥用了吗?
其实还是有点用的,就是你希望别人给你的是有值的,但是别人还是有可能给你nil,这种情况就可以使用Int!。
就比如你开发了一个第三方框架,使用Int!就是希望别人给你的就是有值的,如果别人传nil,你就直接崩掉,如果有值,你就可以直接拿到这个值进行操作,也不用解包。当然第三方框架一直崩来崩去也不好,所以还是不要使用Int!。
七. 字符串插值
可选项在字符串插值或者直接打印时,编译器会发出警告:
至少有3种方式消除警告:
一般使用最后一种方式。
八. 多重可选项
1. 有值
var num1: Int? = 10 //包装Int类型的可选类型
var num2: Int?? = num1 //包装Int?可选类型的可选类型
var num3: Int?? = 10 //可能你会想,右边不应该传Int?类型的吗,其实传10编译器会自动帮我们包装的
他们内部结构如下:
可以发现,num2和num3是完全一样的,也可以通过打印来验证:
print(num2 == num3) //true
可以这样理解:num2和num3都有值10,并且类型都是Int??,所以当然相等啦!
2. 没有值
如果没有值,那么就不一样了,如下:
var num1: Int? = nil
var num2: Int?? = num1
var num3: Int?? = nil
他们内部结构如下:
打印发现num2和num3不一样,如下:
print(num2 == num3) // false
可以这样理解:虽然他们类型相等,但是num2里面有个可选类型,num3里面为空,所以肯定不相等啦!
下面代码值是多少?
(num2 ?? 1) ?? 2 // 2
首先num2不为nil,所以返回num2,又因为1是Int类型,所以会对num2解包,解包之后发现为nil,nil ?? 2,所以最后结果为2。
(num3 ?? 1) ?? 2 // 1
num3为nil,所以返回1,1 ?? 2,所以最后结果为1。
3. 使用lldb指令 frame variable –R 或者 fr v –R 查看内存结构
运行如下代码,打断点:
var num1: Int? = 10
var num2: Int?? = num1
var num3: Int?? = 10
print("Hello, World!") //打断点
执行help frame指令:
(lldb) help frame
Commands for selecting and examing the current thread's stack frames.
Syntax: frame <subcommand> [<subcommand-options>]
The following subcommands are supported:
info -- List information about the current stack frame in the
current thread.
recognizer -- Commands for editing and viewing frame recognizers.
select -- Select the current stack frame by index from within the
current thread (see 'thread backtrace'.)
variable -- Show variables for the current stack frame. Defaults to all
arguments and local variables in scope. Names of argument,
local, file static and file global variables can be
specified. Children of aggregate variables can be specified
such as 'var->child.x'. The -> and [] operators in 'frame
variable' do not invoke operator overloads if they exist,
but directly access the specified element. If you want to
trigger operator overloads use the expression command to
print the variable instead.
It is worth noting that except for overloaded operators,
when printing local variables 'expr local_var' and 'frame
var local_var' produce the same results. However, 'frame
variable' is more efficient, since it uses debug
information and memory reads directly, rather than parsing
and evaluating an expression, which may even involve JITingand running code in the target program.
可以发现,后面跟variable可以查看内存结构,
执行help frame variable指令:
(lldb) help frame variable
......
Command Options Usage:
frame variable [-AFLORTacglrst] [-y <name>] [-z <name>] [-f <format>] [-G <gdb-format>] [-d <none>] [-S <boolean>] [-D <count>] [-P <count>] [-Y[<count>]] [-V <boolean>] [-Z <count>] [<variable-name> [<variable-name> [...]]]
......
-R ( --raw-output )
Don't use formatting options.
......
可以发现使用-R就可以不使用格式化选项,就是内存结构原封不动的打印出来。
执行简化的指令:fr v -R num1、fr v -R num2、fr v -R num3
(lldb) fr v -R num1
(Swift.Optional<Swift.Int>) num1 = some {
some = {
_value = 10
}
}
(lldb) fr v -R num2
(Swift.Optional<Swift.Optional<Swift.Int>>) num2 = some {
some = some {
some = {
_value = 10
}
}
}
(lldb) fr v -R num3
(Swift.Optional<Swift.Optional<Swift.Int>>) num3 = some {
some = some {
some = {
_value = 10
}
}
}
可以发现,num1、num2、num3都是some,说明都不为空,打印出来的内存结构也和我们上面的结构图一模一样的。
如果是nil呢?
var num1: Int? = nil
var num2: Int?? = num1
var num3: Int?? = nil
print("Hello, World!") //打断点
执行指令后打印:
(lldb) fr v -R num1
(Swift.Optional<Swift.Int>) num1 = none {
some = {
_value = 0
}
}
(lldb) fr v -R num2
(Swift.Optional<Swift.Optional<Swift.Int>>) num2 = some {
some = none {
some = {
_value = 0
}
}
}
(lldb) fr v -R num3
(Swift.Optional<Swift.Optional<Swift.Int>>) num3 = none {
some = some {
some = {
_value = 0
}
}
}
可以发现,num1、num3为none,所以none后面的东西都没有意义了,忽略不考虑。num2为some,它里面包装的是:
none {
some = {
_value = 0
}
}
其实就是值为nil的空盒子,num2的内存结构也和我们上面的结构图一模一样的。
九. 可选项的本质
可选项的本质是enum类型,只有none、some两个case。
Swift源码中可选项的定义如下:
public enum Optional<Wrapped> : ExpressibleByNilLiteral {
case none
case some(Wrapped) //关联值,关联什么值和上面的泛型有关
public init(_ some: Wrapped)
}
既然知道可选项本质了,所以可选项可以这么写:
var age: Int? = 10 //本质如下
var age0: Optional<Int> = Optional<Int>.some(10) //最具体
var age1: Optional = .some(10) //省略泛型类型
var age2 = Optional.some(10) //省略类型
var age3 = Optional(10) //使用初始化器,接收一个包装的值
age = nil //本质如下
age = .none
var age: Int? = nil //本质如下
var age = Optional<Int>.none
var age: Optional<Int> = .none
或者混着使用:
var age: Int? = .none
age = 10
age = .some(20)
age = nil
可选项也是支持绑定的
switch age {
case let v?: //如果age有值,就将age解包赋值给v
print("some", v) //这时候v就是Int类型,而不是Int?类型
case nil: //如果age为nil,就会来到这里
print("none")
}
//同理,也可以使用枚举,这种方式更容易理解
switch age {
case let .some(v):
print("some", v)
case .none:
print("none")
}
如果可选项有两个?,那么它本质就是:
var age_: Int? = 10
var age: Int?? = age_ //本质如下
var age = Optional.some(Optional.some(10))
var age: Optional<Optional> = .some(.some(10))
age = nil //本质如下
age = .none
var age: Int?? = 10 //本质如下
var age: Optional<Optional> = 10