Swift中可选类型的理解

可选类型

可选类型(Optional) 是 Swift 语言中比较难以理解的东西之一。

在 Swift 中,普通的变量或者常量初始化之后必须要有值,不能给它们赋空值。比如下面这样子的语句就会报错:

1
2
var str:String = nil
// 报错:Nil cannot initialize specified type 'String'

这里要提一下nil

Swift 中的nil和 Objective-C 中的nil并不一样。在 Objective-C 中,nil是一个指向不存在对象的指针。在 Swift 中,nil不是指针——它是一个确定的值,用来表示值缺失。任何类型的可选状态都可以被设置为nil,不只是对象类型。

上面这段话摘自《The Swift Programming Language 2.0》中文翻译版。上面提到了任何类型的可选状态都可以被设置为nil。类型的可选状态即Type?,这就是所谓的可选类型。

那可选类型到底有什么用呢?上面提到了 Swift 中普通变量初始化之后必须要有值不能为空值,但有的时候变量需要初始化,但初始化的时候又没有值,那怎么办呢?这个时候可选类型就派上用场了。

可选类型(Optional)其实是个enum,里面有NoneSome两种类型。其实所谓的nil就是Optional.None, 非nil就是Optional.Some, 然后会通过Some(T)包装(wrap)原始值,这也是为什么在使用 Optional 的时候要拆包(从 enum 里取出来原始值)的原因, 也是 PlayGround 中打印 Optional 值会显示为类似Optional("Hello World")的原因。下面是enum Optional的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
enum Optional<T> : LogicValue, Reflectable {
case None
case Some(T)
init()
init(_ some: T)

/// Allow use in a Boolean context.
func getLogicValue() -> Bool

/// Haskell's fmap, which was mis-named
func map<U>(f: (T) -> U) -> U?
func getMirror() -> Mirror
}

可选类型通过在类型后面加上一个?来声明:

1
2
var optionalStr: String?   //?相当于下面这种写法的语法糖
var optionalStr: Optional<String>

这句语句声明了一个可选类型(Optional)的变量str,同时它自动完成了初始化,并且有初始值nil。这里我并不是声明了一个可选的 String 类型变量,而是变量的类型就是可选类型(Optional)。它表示“str这个可选类型的变量,可能包含一个 String 类型的值,也可能什么都没有(nil)”。

我们可以使用 if 语句来判断一个可选值是否包含值。注意,Swift 中,if 语句的条件必须是一个布尔表达式,这意味着下面这样的语句会报错:

1
2
3
if optionalStr {
}
// 报错:Optional type 'String' cannot be used as a boolean; test for '!=nil' instead

所以正确的写法应该是这样:

1
2
if optionalStr != nil {
}

假设我们给之前定义的变量 str 分别赋值为 “hello world” 和 nil,先把 nil 赋值语句注释掉,再使用 if 语句判断并打印相关值:

1
2
3
4
5
6
7
8
optionalStr = "hello world"
//optionalStr = nil

if optionalStr != nil {
print(optionalStr)
} else {
print("no value")
}

nil 赋值语句注释掉的情况下,上面的代码会输出Optional("hello world")\n
nil 赋值语句取消注释的情况下,上面的代码会输出no value\n

有时候,我们确定可选类型确实包含值,那我们可以在可选类型变量的名字后面加上一个!来获取这个值。这个感叹号表示“我知道这个可选有值,请使用它。”这被称为可选值的强制解析
例如上面的例子中,nil 赋值语句注释掉的情况下,那我们确定变量 str 肯定有值,那 if 语句就可以这么写:

1
2
3
4
5
if optionalStr != nil {
print(optionalStr!)
} else {
print("no value")
}

这个时候,打印语句就直接输出hello world\n,而不像之前Optional(...)的形式。

注意:使用!来获取一个不存在的可选值会导致运行时错误。使用!来强制解析值之前,一定要确定可选包含一个非nil的值。

还有一种方法用来使用可选值,即可选绑定。所谓可选绑定,就是用 if 语句判断一个可选类型的变量是否包含值,如果包含,就把它里面包含的值取出来赋给一个临时的常量或者变量,我们可以在 if 语句中直接使用这个临时的常量或者变量,这个常量或者变量已经不是可选类型了,它的类型就是可选类型变量中包含的那个值的类型。将可选变量里面包含的值取出来的过程,称之为拆包。
可选绑定形如下面这样:

1
2
3
if let str = optionalStr {
print(str)
}

接上例,由于可选变量 optionalStr 包含字符串类型的值,所以此处将可选变量拆包,取出该字符串值赋值给临时常量 str 。所以上面的打印语句输出hello world\n。不能在 str 后面加上!,因为它不是一个可选类型。

隐式解析可选类型

有时候,我们在第一次赋值之后,可以确定一个可选类型总是会有值,在这种情况下,每次都要判断和解析可选值是非常低效的,因为我们可以确定它总是会有值。
这时隐式解析可选类型(implicitly unwrapped optionals)就出场了,声明和定义隐式解析可选类型很简单,只要把普通可选类型的?换成!即可。
这里首先要注意的是,隐式解析可选类型也是可选类型,只不过,它被当成了非可选类型来使用。为什么可以这么使用呢?那是因为每次使用的时候,系统自动帮我们把值取出来了。就好像系统每次自动帮我们加上了!一样,反正确定是有值的,所以每次用的时候,就把值拆包取出来,这样我们不用每次手动去拆包。
所以,我们不能在隐式解析可选类型没有值的使用使用它,因为一使用,就直接拆包,结果发现没有值,那就会触发错误。
下面比较可选类型和隐式可选类型的区别:

1
2
3
4
5
let possibleString: String? = "An optional string."
let forcedString = possibleString! // 需要感叹号来取值

let assumedString: String! = "An implicitly unwrapped optional string."
let implicitString = assumedString // 不需要感叹号来取值

由于隐式可选类型就是一个可选类型,所以它依然可以用 if 语句判断它是否有值,依然可以使用可选绑定。

可选类型调用其他属性方法

当一个可选类型要调用其他方法或者访问其他属性的时候,直接变量名后面使用点语法是会报错的:

1
2
3
4
if optionalStr != nil {
let hashValue = optionalStr.hashValue
}
// 报错:Value of optional type 'String?' not unwrapped; did you mean to use '!' or '?'?

上面的错误提示说的很明显,我们还没有给这个可选类型拆包,我们可以在 optionalStr 后面加上!或者?来使用,即可空链式调用:

1
2
3
let hashValue = optionalStr!.hashValue

let hashValue = optionalStr?.hashValue

!表示“我确定这个变量肯定包含值,请使用它”,如果事实上它没有包含值,那就会报错。
?表示“如果这个变量包含值,请使用它,否则就不使用”。

可空链式调用的返回结果与原本的返回结果具有相同的类型,但是被包装成了一个可选类型值。当可空链式调用成功时,一个原本返回Int的类型的结果将会返回Int?类型。如果调用返回nil则说明调用失败。

可空链式可以多层链接,多层可空链接不会增加返回值的可空性,即不会出现:原来的返回值是一次 optional,调用后变成了两次optional。

结合上面的隐式可选类型,我们应该可以知道,隐式可选类型可以像普通类型一样直接调用,不需要变量后面加!?,但是一旦这个隐式可选类型没有值,我们去使用就会报错。

向下转型

类型向下转型可能会失败,所以也要用到可选类型,返回可选值。向下转型使用as关键字,实际使用时,用as?as!!?的用法和上面一样。

1
2
if let movie = item as? Movie {
}

参考

http://joeyio.com/ios/2014/06/04/swift—/