Optional
Optional是Swift编程语言非常具有特色的语法之一,这在当前大部分编程语言中是不具备的。Optional是Swift编程语言对类型安全的一种体现。Optional在Swift中属于一种类型的类别(category),就好比C语言中的指针和数组作为类型的类别一样。当我们在某一类型后面紧跟着 ? 或 ! 即表示当前对象类型为一个Optional类型。比如,Int? 、Float! 都属于Optional类型。这里各位需要注意的是,? 必须紧贴着 Int,! 必须紧贴着 Float,中间不能留有任何空白符。为何Swift会引入Optional语法特性呢?因为它具备以下这些特性:
- 只有Optional类型才能作为空值(nil)。这样能确保非Optional的对象不空,我们也无法将它们置空。
- Swift中任何类型都能作为Optional类型,甚至是元组!比如:let tuple: (Int, Float)? = nil; 而这在许多其他编程语言中是所不具备的。比如:Java中, int、 float 等基本类型不能置空;Objective-C中只有Objective-C类类型的对象引用才能置空。所以在这类编程语言中,如果函数的返回类型涉及到不能置空的情况,往往会用一个所谓的“无效值”,比如 NSNotFound(即(NSUInteger)-1)。用某一数值作为无效值在绝大部分情况下都没有问题,但显然没有直接以空作为无效值来得体面。
- Optional提供了一种非常简洁的表达可选执行表达式的方式。我们在访问一个Optional对象时,包括访问其实例属性或实例方法时,我们无需每次都用if语句去判断当前Optional对象是否为空,而直接使用Optional链表达式(optional-chaining expression)即可!而这种形式有一个非常棒的好处,比如用于异步函数执行时,当前函数中所使用的某个全局对象是否被其他模块或其他线程置空了,我们就无需有此担心,也不用关心当前该对象的值是否为空,只要凡是对它访问时都直接使用optional-chaining操作符即可。如果Optional对象为空,那么整个表达式不会被执行,并且整个表达式也会返回空。
Optional的基本使用
我们什么时候使用Optional呢?比如我们在声明一个对象时倘若无法确定其当前值,那么可以先将它置空,稍后为它赋值。这跟推迟初始化还是有区别的。尽管推迟初始化同样用于在某个位置处确定该对象的值,但在那个位置之前,该对象不应该被任何其他对象所引用,否则会引发编译错误。而如果我们在某处需要引用一个对象,而该对象当前尚未被初始化,此时我们就可以使用Optional了。
当我们声明一个Optional对象时,无论该对象是在文件作用域还是在函数体内作为局部对象声明,如果不对它初始化,那么它的值默认为空(nil)。Swift中,空值 nil 与表示“虚无” 的空元组 () 还是有很大区别的。nil 作为空值可用于任一Optional对象,仅表示该对象当前值为空(或称为“无效的”),但是我们仍然可以用一个有效的对象值或者引用对它进行赋值。而空元组 () 的类型就是 Void,而 () 是 Void 类型的唯一“值”。我们先看一些简单的例子。
// 声明a为一个Int类型的Optional对象,
// 并用100对它进行初始化
var a: Int? = 100
// 声明f为一个Float类型的Optional对象,
// 并用空对它进行初始化
var f: Float? = nil
// 声明s为一个String类型的Optional对象,
// 没有对它显式做立即初始化,
// 不过其值会被默认设置为空
var s: String?
// 我们可以用 == 以及 != 比较操作符来判定一个Optional对象是否为空
if s == nil {
print("Yes!")
}
// 这里声明了一个Double类型对象d,
// 但没有对它直接初始化
let d: Double
// 这条语句编译错误:
// 在常量d使用之前未被初始化
f = Float(d)
// 这条语句没问题,
// 并且b的类型也被推导为 Float? 类型
let b = f
// 这里使用Int类型的Optional初始化器构造一个值为100,
// 类型为 Int? 的Optional对象。
// 常量c的类型也是 Int?
var c = Int?(100)
// 这里声明一个Optional的数组对象arr,
// 然后用一组数组字面量对它初始化
var arr: [Int]? = [1, 2, 3, 4]
// 这里声明了一个Optional的数组对象arr2,
// 其每个元素的类型是一个 Int? 的Optional类型,
// 随后用一个数组字面量作为其初始化器的实参
var arr2: [Int?]? = [Int?]?([1, 2, nil, 3])
// 这里声明了一个常量Optional元组,
// 其第一个元素为 Int? 类型;
// 第二个元素为Float类型;
// 第三个元素为 Double? 类型
let tuple: (Int?, Float, Double?)?
上述代码中可以看到,当用一个Optional对象直接为另一个对象初始化时,在赋值操作符左侧的对象也应该是一个Optional对象。同时,我们可以使用 == 以及 != 操作符来判定当前一个Optional对象是否为空。当然 nil 与对象表达式都能作为这两个比较操作符的左操作数或右操作数,所以我们用 nil == s 也完全没问题,不过与空值进行比较的表达式必须是一个Optional类型。之前已经提到了,Swift的类型系统是一个十分完备的系统,因此不会有任何无中生有的类型出现,这里的Optional也是如此!一个Optional类型,其本质是一个遵循 ExpressibleByNilLiteral 协议的 Optional 泛型枚举类型。因此,当我们要表达一个 Int? 类型,完全可以使用 Optional<Int> 。我们可以看以下代码。
// 声明一个Optional常量a,其类型为Int?
// 随后被直接初始化为100
let a: Optional<Int> = 100
// 声明一个Optional变量f,
// 显式对它初始化为空
var f = Optional<Float>(nilLiteral: ())
if f == nil {
print("Yes!")
}
// 当然,我们还是应该用类似以下方式
// 来显式初始化一个对象为空
var g: Optional<Float> = nil
// 声明一个Optional常量d,
// 这里使用了Optional枚举中的一个枚举值none,
// 它一般即表示为nil
let d = Optional<Double>.none
if d == nil {
print("Yes!")
}
// 声明一个Optional常量s,其类型为String?
// 随后用"Hello"字符串字面量为它初始化
let s = Optional<String>("Hello")
// 这里会打印出:s = Optional("Hello")
print("s = \(String(describing: s))")
Optional链
由于Swift编程语言总的来说是一门面向对象的编程语言,因此几乎所有类型的实例都可以看作为一个对象,用该对象访问其实例属性以及实例方法。当我们要访问一个Optional对象的实例属性或实例方法时,或者直接对它做一些算术逻辑计算时,我们需要显式地对它加上optional-chaining操作符 ?,表示当前对象的值可能为空。
这里大家要注意的是,optional-chaining操作符 ? 是一个单目后缀操作符,并且它必须紧跟在其操作数表达式之后,两者之间不能有任何空白符。否则 ? 会被当成[条件操作符]使用。 optional-chaining操作符 ? 的操作数必须是一个遵循 ExpressibleByNilLiteral 协议的类型的对象。我们将一个Optional对象用作为某一双目操作符的左操作数时可以使用 optional-chaining操作符 ? 来判定当前表达式是否执行,倘若当前Optional对象不空,那么该表达式将会执行,否则不会执行。而Optional对象往往无法作为前缀单目操作符的操作数、双目操作符的右操作数以及三目操作符的操作数,这点也请各位留意。
// 这里声明Optional变量a,
// 类型为 Int? ,初始化为空
var a: Int? = nil
// 这里使用optional-chaining操作符,
// 表示如果a不为空,则执行a = 10操作,
// 否则不执行任何操作。
// 这里表达式 a? 作为 = 赋值操作符的左操作数
a? = 10
// 这条语句同上面一样
a? += 10
// 这里输出:a = nil
print("a = \(String(describing: a))")
// 为Optional对象a进行赋值,
// 这里与上面的 a? = 10 所不同的是,
// 这里的赋值操作肯定执行
a = 10
// 当Optional对象用作 += 的左操作数时,
// 这里的 ? 不能省
a? += 20
// 这里输出:a = Optional(30)
print("a = \(String(describing: a))")
// 声明一个Optional的数组对象arr
var arr: [Int]? = [1, 2, 3]
// 这里表示如果arr不空,
// 那么就将[4, 5, 6]添加到它后面
arr? += [4, 5, 6]
// 这里使用optional-chaining操作符表示,
// 当arr这一Optional数组不空时,
// 对它索引为1的元素加上2,
// 这里表达式 arr? 作为下标操作符的操作数
arr? [1] += 2
// 输出:arr[1] = Optional(4)
print("arr[1] = \(String(describing: arr?[1]))")
// 这里刻意在成员操作符 . 两边留有空格,
// 以示我们常用的 ?. optional-chaining成员访问操作的实质
// 这里输出:arr length: Optional(6)。
// 这里表达式 arr? 作为成员访问操作符 . 的操作数
print("arr length: \(String(describing: arr? . count))")
// 这里声明了一个 () -> String? 函数类型的Optional常量引用ref
let ref: (() -> String?)? = {
print("This is a closure!")
return "Hi"
}
// 这里声明了一个常量count,
// 根据初始化器的类型进行推导,
// 这里count常量的类型为 Int?
// 这里各位注意看optional-chaining操作符的位置,
// ? 与 () 函数调用操作符的位置以及成员访问操作符的位置。
// 这里与它们之间都可以用空白符分隔。
// 此外我们还能注意到,
// optional-chaining操作符的操作数可以用圆括号进行分隔。
// 这里表达式 ref? 作为函数调用操作符的操作数;
// 表达式 ( ref?() ) 作为optional-chaining操作符 ? 的操作数;
// 表达式 ( ref?() )? 作为成员访问操作符 . 的操作数
let count = (ref? ())? .characters.count
// 这里输出:count = Optional(2)
print("count = \(String(describing: count))")
通过上述代码片段我们可以看到,optional-chaining操作符 ? 在表达式中对某一Optional对象或Optional表达式的操作情况,包括其摆放位置,对整个表达式所起到的作用等。一般来说,当我们在表达式中使用optional-chaining操作符时,整个optional-chaining表达式只能作为赋值操作符(=)以及复合赋值操作符(+=、<<=、 &= 等)的左操作数,下标操作符的操作数,函数调用操作符的操作数,以及成员访问操作符的操作数。此外,optional-chaining表达式可以作为左值使用。
对Optional对象的操作要求非常严苛,我们往往需要显式添加optional-chaining操作符来对它进行操作,而一般只有赋值操作符才能直接作用于一个Optional对象,或者将它作为某一函数的Optional类型形参的实际参数进行传递。
Optional的强制拆解
我们之前已经提到了不少有关Optional的语法特性。我们在之前的一些代码片段中也能观察到,在很多时候我们由于起初无法确定某一对象的值,因而才把它设置为一个Optional类型的对象,那么从代码中的某一处开始起,我们已经确保该对象不会再为空了,此时我们就可以对该Optional对象拆解为普通的对象。这就好比原本用 Optional<Int> 枚举类型封装的一个 Int 类型对象拆解为一个普通的 Int 类型对象。
要对一个Optional对象进行拆解非常容易,我们只需要对此Optional对象使用 optional-chaining操作符 !。这里的 ! 与之前的 ? 一样,都属于单目后缀操作符,它必须紧跟在其操作数之后,中间不能留有任何空白符,而且两者均属于 optional-chaining操作符。此外,! 的操作数也必须是一个遵循 ExpressibleByNilLiteral 协议的类型的对象。不过这里 ! 的语义显然跟 ? 有明显的差异。? 表示其操作数表达式可能为空,因此需要提示编译器生成相关运行时保护代码做相应处理。而 ! 则表示其操作数表达式一定不为空,并且整个optional-chaining表达式的返回结果为 ! 操作数的Optional拆解之后的对象。下面我们来看一些例子。
// 这里声明了一个Optional变量a
var a: Int? = 100
// 由于这里我们已经确保了变量a不会为空,
// 所以使用强制拆解以获得一个普通 Int 类型的对象
let ca = a!
// 这里输出:ca = 100
// 说明ca常量的类型即为Int
print("ca = \(ca)")
// 我们这里将对象a的值做一次乘方计算。
// 由于 * 的左右操作数不能是Optional类型,
// 所以我们这里使用强制拆解,
// 使得 a! 表达式得到的是一个 Int 类型对象
a! = a! * a!
// 输出:a! = 10000
// 说明 a! 表达式的类型即为 Int
print("a! = \(a!)")
// 这里声明了一个Optional的数组变量arr
var arr: [Int]? = [1, 2, 3, 4]
// 这里使用强制拆解,使得carr的类型为 [Int]
let carr = arr!
// 这里输出:carr = [1, 2, 3, 4]
print("carr = \(carr)")
// 这里断言arr不空,
// 然后将arr的索引1的值与索引2的值相加,
// 最后赋值给索引0的值
// 这里我们再注意一下 ! 与 [] 操作符之间的空格
arr! [0] = arr! [1] + arr! [2]
// 这里输出:arr![0] = 5
print("arr![0] = \(arr![0])")
// 声明了一个Optional的指向() -> Void函数类型的引用变量
var ref: (() -> Void)? = {
print("This is a closure!")
}
// 使用强制拆解,
// 使得cref为 () -> Void 类型
let cref = ref!
cref()
// 这里直接对ref进行拆解调用。
// 注意 ! 与 () 操作符之间的空格
ref! ()
// 声明一个字符串类型的Optional变量s
var s: String? = "abcd"
// 这里先对s做Optional拆解,
// 使得 s! 为一个String对象,
// 然后访问其实例属性,
// 因此整个表达式的结果为Int类型
let count = s! .characters.count
// 这里输出:count = 4
print("count = \(count)")
上述代码例子详细介绍了Optional强制拆解的使用以及效果。
Optional绑定
Optional绑定的语法形式跟C语言的语法特征很像,通过if语句或guard语句后面跟完整的带有初始化器的对象声明。比如:
if let obj = Optional(0) {
}
或:
if var obj = Optional(0) {
}
表示:在if语句中声明了一个对象obj,并且用 = 后面的一个Optional对象表达式对它进行初始化;倘若obj不空,那么执行这里的if语句块中的内容,否则跳过if语句块的执行。另外,obj的类型为Optional拆解后的类型。这里各位要注意的是,在使用if语句进行Optional绑定的时候,所声明的对象(这里是obj)仅对if语句块可见,并可以覆盖掉if语句外部的对象标识。下面我们举一些例子进行说明。
var a = Optional(0)
if let c = a {
// 这里输出:a = 0
print("a = \(c)")
}
// 这条if语句的描述是:
// 声明一个if语句作用域的局部变量a,
// 用if语句外的Optional对象a对它初始化
if var a = a {
// 这里所访问的a是if语句所声明的变量a,
// 由于声明的a是个变量,因此可以对它进行修改
a += 10
print("a = \(a)")
}
else {
// 如果if声明中 = 右边的操作数表达式a为空,
// 那么执行这里的else语句块
a = 0
// 这里所访问的是if语句外的Optional对象a,
// 因此可以对它使用optional-binding操作符 ?
a? += 5
print("null!")
}
print("a = \(a!)")
var x: Int? = 10
var y: Int? = nil
// 我们也能在Optional绑定的if声明中使用多个绑定局部对象
// 这时,每个声明的Optional绑定都需要用let或var引出。
// 此时,多个声明之间的关系是“并且”的关系,
// 倘若其中一个Optional对象为空,
// 那么当前if条件则不成立。
if let x = x, let y = y {
// 这里的if语句不会被执行,
// 因为外部Optional对象y为空
print("First, x = \(x), y = \(y)")
}
y = 20
if let x = x, var y = y {
y += 10
// 输出:Second, x = 10, y = 30
print("Second, x = \(x), y = \(y)")
}
通过if语句的Optional绑定其实就类似于以下这种表达:
var a = Optional(0)
// 这里的Optional绑定
if let c = a {
print("a = \(c)")
}
// 相当于:
if a != nil {
let c = a!
do {
print("a = \(c)")
}
}
所以Optional绑定其实是一种简便表达形式的语法糖(syntax sugar)而已。
下面谈谈guard语句的Optional绑定。guard语句的Optional绑定与if语句类似,这里的区别有两个,一个就是guard语句与if语句本身的语义区别;还有一个是guard语句中声明的对象是处于guard语句所在的作用域的,而不仅限于guard语句块作用域。所以guard语句的Optional绑定在实际项目中可能会用得更多些,因为它更加便利。下面来看一些例子。
var a = Int?(0)
// 这里使用了guard语句的Optional绑定
// 这里声明的c在当前作用域可见
guard let c = a else {
exit(0)
}
// 这里可直接打印c的值
print("c = \(c)")
// guard语句非常厉害的特性就是
// 能将之前声明的Optional对象给完全覆盖掉
guard var a = a else {
exit(0)
}
// 这里所用的a就是一个Int类型,
// 而不是 Int? 类型
a += 100
// 这里输出:a = 100
print("a = \(a)")
我们可以看到,guard语句的Optional绑定非常强大的特性就是可将其声明的拆解掉Optional的对象将之前所声明的Optional对象标识给完全覆盖掉。后面所使用的都是经过guard语句处理后拆解好的对象。所以这个语法糖比if语句的Optional绑定威力更大,所以各位可以多加利用。
此外, while 语句也能作为Optional绑定语法来使用,尽管这没有什么实践意义。
var a: Int? = nil
while let a = a {
}
空结合操作符
空结合操作符(nil-coalescing operator)?? 是另一种既轻量又便捷的拆解Optional对象的方式。空结合操作符是一个双目操作符,其左操作数必须是一个遵循 ExpressibleByNilLiteral 协议的类型的对象,右操作数可以是任一类型的对象,可以是Optional的,或非Optional的。整个空结合表达式( nil-coalescing expression)所表达的语义如下:
var a = Int?(0)
let b = 0
// 这里的 a ?? b 表达式
var c = a ?? b
// 相当于:
c = a != nil ? a! : b
这空结合表达式也是一种简便表达形式的语法糖,它可以用三目条件表达式来代替。空结合操作符的返回类型为 ?? 右操作数表达式的类型。
var a = Int?(0)
var b = a ?? -1
// 这里输出:b = 0
print("b = \(b)")
// 这里将a置空
a = nil
b = a ?? -1
// 这里输出:b = -1
print("b = \(b)")
let s: String? = "abcd"
// 由于 ?? 操作符的右操作数是一个 Int? 的Optional类型,
// 所以这里c的类型会被推导为 Int? 而不是 Int
let c = s?.count ?? a
// 这里输出:Optional(4)
print("c = \(String(describing: c))")
隐式拆解的Optional类型
之前我们所谈论的内容都是围绕着 Optional 这一类型,其实Swift关于Optional类型中还有一个类型,称为 ImplicitlyUnwrappedOptional。它与 Optional 一样,也是遵循 ExpressibleByNilLiteral 协议的泛型枚举类型。在当前版本的Swift编程语言中,也就 Optional 与 ImplicitlyUnwrappedOptional 这两个枚举类型遵循了 ExpressibleByNilLiteral 协议,因此可以将它们作为optional-chaining操作符 ! 与 ? 的操作数,作为空结合操作符的左右操作数,可以赋空值,可用于Optional绑定。
隐式拆解的Optional类型的表示与Optional类似,就是在类型名后紧跟 !。比如, Int! 、Float! 、String!、[Float]!、(Int, Float)! 等等。它也表示当前对象作为一个Optional类型,不过在使用此对象时,它已经被断言确保不为空了。因此当它在某些情况下作为一个左值表达式使用时,就好比已经对它做了Optional强制拆解,尽管我们再对它使用optional-chaining操作符也不会有问题;而当它作为右值表达式使用时,仍然返回为一个 Optional 类型的对象,这里请大家注意。下面我们来看一些代码示例。
// 这里声明了一个隐式拆解的Optional变量a
var a: Int! = 10
// 这里b的类型为Int?
let b = a
// 这里输出:b = Optional(10)
print("b = \(String(describing: b))")
// 当隐式拆解的Optional对象作为复合赋值操作符的左值时,
// 仍然需要使用 ! 或 ? optional-chaining操作符
a! += 20
a? += 5
// 这里声明了一个隐式拆解的Optional数组变量arr
var arr: [Int]! = [1, 2, 3]
// 这里对arr使用 ! 操作符,
// 使得 arr! 表达式作为 += 的左操作数
// 这里的 ! 不能缺省
arr! += [4, 5, 6]
// 这里就彰显出隐式拆解的Optional的便捷性了。
// 这里可直接将arr作为下标操作符的操作数,
// 而无需使用optional-chaining操作符
arr[0] = arr[1] + arr[2]
// 这里需要使用强制拆解,
// 因为 arr 作为 += 的右操作数时,
// 它将会转回 Optional 类型,这里需要各位留意
arr! [0] += arr! [3]
// 输出:arr[0] = 9,
// 说明arr[0]是Int类型
print("arr[0] = \(arr[0])")
// 这里声明了carr,其类型为[Int]?
var carr = arr
// 这里对carr操作时,? 或 ! 都不能缺省
carr![3] = carr![2] - carr![1]
// 这里声明了一个指向 () -> Void 函数类型的
// 隐式拆解的Optional的引用常量ref
let ref: (() -> Void)! = {
print("This is a closure!")
}
// 这里可直接对ref做函数调用,
// 无需使用optional-chaining操作符
ref()
// 这里声明了一个String类型的
// 隐式拆解的Optional常量s
let s: String! = "abcd"
// 这里可直接对s使用成员访问操作符,
// 而不需要添加optional-chaining操作符,
// c的类型为Int
let c = s.count
// 输出:c = 4
print("c = \(c)")
// 如果我们使用了optional-chaining操作符 ?
// 那么整个表达式就变成了optional-chaining表达式了,
// 这里count的类型就变为了 Int?
let count = s?.count
// 输出:count = Optional(4)
print("count = \(String(describing: count))")
// 这里请务必注意!
// 由于之前已经提到了,
// 一个隐式拆解的Optional类型,即ImplicitlyUnwrappedOptional,
// 当它作为右值表达式时,其类型会被隐式转为Optional。
// 所以这里的option的类型为 String? ,而不是 String!
let option = ImplicitlyUnwrappedOptional("abc")
// 这句没有问题
_ = option?.count
// 这里就会出现编译报错:
// Optional类型 String? 的值没有被拆解
_ = option.count
我们在上述代码例子中可以看出隐式拆解的Optional类型与一般Optional类型的异同点。隐式拆解的Optional类型一般用于结构体或类的实例属性,比如我们将某个图像视图控件作为某个视图控制器类的实例属性。但是在声明它的时候由于不知道当前视图控制器的实际尺寸,也不知道所用的图像,因此无法对它进行初始化,而只有等到调用 viewDidLoad方法之后才能对它初始化。那么这个时候,该图像视图对象就可以声明为隐式拆解的Optional类型,后续对它成员的访问就不需要显式样使用optional-chaining操作符了,代码也能更简洁一些。