学习Swift时,绝大多数初学者都会被Swift中的Optional这个类型所困扰,比如“问号是什么?”、“叹号是什么?”、“为什么这里要加问号/叹号?”等等,这篇文章将会结合本人的一些经验来谈谈Swift的Optional类型。
为什么会有Optional类型
Swift是一门静态语言,在Swift中,所有的变量常量都需要在编译期间确定类型,在确定类型之后,编译器就可以检查代码中的一些错误,比如我们在代码中将一个String类型的值赋给一个UIView类型的变量时,编译器会直接报错。
var view: UIView = UIView() view = "ABC" // 编译失败
而Objective-C是一门动态语言,这个操作在中是允许的。
UIView *view = @"ABC"; // 编译和运行都不会报错,但在调用UIView的属性或方法则会crash
静态语言会在编译期间检查类型的合法性,使用静态语言我们可以依赖编译器来检查出一些不符合预期的问题,在运行时A类型就是A类型,不会出现牛头不对马嘴的错误。
Optional的本质
那么在Swift中,nil算是什么?nil既不是String类型也不是UIView类型,我们无法保证所有的变量都必定会有值,开发的时候不可避免地需要使用到nil要怎么办?Swift给出了这样一个解决方案,那就是Optional类型,我们先来看看Optional的本质是什么,下面是Optional类型的声明(在Xcode中打出Optional再按住command点击即可查看)
public enum Optional<Wrapped> { /// The absence of a value. /// /// In code, the absence of a value is typically written using the `nil` /// literal rather than the explicit `.none` enumeration case. case none /// The presence of a value, stored as `Wrapped`. case some(Wrapped) }
可以看到,Optional是带范型的枚举类型,该枚举类型有两种值分别是none和some,some类型还带了一个参数,从注释里我们可以看到,nil其实就是Optional枚举中的none这个值,那么到这里要理解l就很简单了,就像是一个用一个箱子来将内容存放起来,箱子里可能放了东西(some),也可能什么也没有(none),并且Swift还提供了一些语法糖来使得Optional使用起来更加方便(比如`XXX?`声明`Optional<XXX>`、双问号`??`提供默认值等)。
Optional类型解包
了解了Optional的本质后可知`String?`和`String`并不是同一个类型,前者实质上是`Optional<String>`而后者是字符串类型`String`,当一个方法需要的参数是`String`类型时传入一个`String?`的话会因为类型不匹配而导致编译器报错。
那我们在开发过程中要如何去通过一个Optional类型的值去取出实际类型的值呢?既然Optional是一个枚举类型,那么我们就可以使用`switch`语句来进行一些判断。
let value: String? = "abc" switch value { case .none: break case .some(let text): print(text.count) // 此处text已经是String类型 }
开个玩笑,其实Swift有提供一些很方便的东西来代替这个`switch`的写法,在介绍这些之前,这里先定义一个方法以便之后使用和讲解。
func printCount(of str: String) { print(str.count) }
使用 if let 或 guard let
Swift提供了一些方法让开发者可以有条件地解开一个Optional得到一个新的变量,比较常见的就是`if let`和`guard let`语句了,上文中提到的`switch`也是有条件解包的一种,但实际开发中不常使用。
func testIfLet(text: String?) { // if let的用法 if let text = text { // 若text不为nil则拆包成功,条件成立执行if内的语句 printCount(of: text) } else { // 若text为nil则执行else内的语句 print("text is nil") } } func testGuardLet(text: String?) { // guard let的用法 guard let text = text else { // 若text为nil则解包失败执行else内的语句并return print("text is nil") return } printCount(of: text) }
`if let`和`guard let`在解包的同时还可以判断这个Optional值是否为nil,至于在开发中需要使用`if let`还是`guard let`就见仁见智了,使用`if let`在解包失败后也可以继续执行剩下的代码,但`if let`使用过多可能会出现if嵌套地狱,而`guard`虽然可以减少嵌套层数,但是`guard`不成立时必须要执行退出当前流程的语句(return或throw等,如果在循环中还可以使用break和continue)。
强制解包
除了`if let`和`guard let`,Swift还提供了强制解包的方法,也就是文章第一段中提到的叹号,使用叹号可以直接解包得到一个非Optional的对象。
func testForceUnwrap(text: String?) { let abc = text! // 这里使用了叹号进行强制解包,因此abc的类型为String而非Optional printCount(of: text) }
强制解包写起来是很方便,但是开发中要谨慎使用,因为当text为nil的时候,解包失败会导致应用crash,Swift的文档对于强制解包的描述最后一句有说明“对一个nil进行强制解包会发生运行时错误”,因此在开发当中,除非能确认某个Optional值在使用时绝对不会是nil,否则请不要使用叹号进行强制解包。
提供默认值
除了通过以上方式进行解包以外,Swift还提供了`??`运算符用于解包并提供默认值。
func testDefaultValue(text: String?) { let abc = text ?? "" // 若text不为nil则解包,若为nil则使用空字符串""作为text的值 printCount(of: abc) }
某些时候我们并不关心某个变量是否为nil,我们只希望代码能不出问题地执行下去,比如上面的代码,当text为nil的时候调用printCount时在控制台中打印0也是可以接受的结果,这时候使用`??`提供默认值要比`if let`或`guard let`以及强制解包要更加合适。
Optional Chaining
有的读者读到这里的时候可能就想问了,“前面解释了叹号的作用,那问号呢?”,不要着急问号这就来了。
在OC中如果某个变量是nil时,调用这个变量则会无事发生,Swift中也提供了一个类似的机制,那就是Optional的问号操作符,使用问号操作符对Optional中的对象进行操作就构成了一个可选链。
class ListNode { var text: String var next: ListNode? } ... func testOptionalChaining(node: ListNode?) { let thirdValue = node?.next?.next?.text if let text = thirdValue { printCount(of: text) } else { print("thirdValue is nil") } }
上边的代码中使用了问号操作符并调用了node的next属性,问号操作符会先判断该node是否为nil,如果不为nil则解包并继续执行,如果是nil则会打断当前的可选链并返回nil,所以虽然ListNode类中的text属性不是Optional类型,但`node?.next?.next?.text`的返回值的却是Optional的。
为了展示可选链被打断的情况,我们给ListNode类增加一个方法并测试一下,看一下结果如何。
class ListNode { var text: String var next: ListNode? func printTestAndGoNext() -> ListNode? { // 在当前方法中打印text的值并返回next节点 print("----node value----") print(self.text) return self.next } } func testBreakOptionalChaining(node: ListNode?) { node?.printTextAndGoNext()?.printTextAndGoNext()?.printTextAndGoNext().?printTextAndGoNext() }
调用testBreakOptionalChaining并且传入了一个链表的头节点,该链表只有两个节点,第一个节点的text值为”a”,第二个节点的text值为”b”,此时控制台输出如下。
----node value---- a ----node value---- b
虽然在代码中链式调用了四次`printTextAndGoNext`方法,但是控制台中植输出了两个值,因为第二个节点的next节点为nil,所以实际上后边的两次调用并没有被执行,这个可选链被打断了。
问号叹号小剧场
上文中介绍了使用叹号进行强制解包和使用问号来写一个可选链,但其实问号和叹号除了在这两个地方会被用到,在声明一个Optional类型的时候也可以使用问号和叹号,下面是声明Optional类型的几种方式。
// 方法1: 直接声明一个Optional<String>类型的变量,需要手动提供默认值 var text1: Optional<String> = nil // 方法2: 使用类型+问号声明一个Optional类型变量,默认值为nil var text2: String? // 方法3: 使用类型+叹号声明一个Optional类型变量,默认值为nil var text3: String!
在开发中并不建议使用方式1来声明Optional类型,应该使用Swift提供的“类型+问号/叹号”声明Optional类型的语法糖。
在声明Optional类型的变量时,问号和叹号的效果也是不一样的,使用问号声明的效果与方法1等同,使用叹号声明则有一些小小的差别,使用叹号声明的Optional类型,在被调用时默认会被强制解包。
print(text1?.count) // 输出nil print(text2?.count) // 输出nil print(text3.count) // 对text3强制解包,应用crash
因此使用“类型+叹号”声明Optional类型的方式在实际开发中应当谨慎使用,当然也不是说完全不能用,总结一下,如果某个变量/属性在使用时随时可能为nil,那么就应该使用“类型+问号”来声明;如果某个变量/属性在开始的时候可能为nil,但是后续使用的时候必定会有值,那么就可以使用“类型+叹号”来声明。
Tips:UIViewController的view属性就是使用叹号声明的(`var view: UIView!`),在init时候为nil,但是随后执行loadView后就有了值。
结语
说了那么多,其实Optional很简单,它就像是一个箱子,在打开箱子之前你不知道里面有没有东西,使用问号就相当于告诉你箱子里可能有东西也可能什么也没有,所以你在打开的时候需要检查一下到底是有还是没有,而使用叹号就相当于告诉你这个箱子里是有东西的,打开的时候不需要检查了,所以你打开箱子的时候看到空空如也的箱子就会产生混乱(运行时错误),并不知道这箱子里的东西是被偷了还是对方骗了你。
Oh, this is the most fantastic blog I’ve ever seen!