浅谈Swift中的Optional类型

学习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 letguard 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很简单,它就像是一个箱子,在打开箱子之前你不知道里面有没有东西,使用问号就相当于告诉你箱子里可能有东西也可能什么也没有,所以你在打开的时候需要检查一下到底是有还是没有,而使用叹号就相当于告诉你这个箱子里是有东西的,打开的时候不需要检查了,所以你打开箱子的时候看到空空如也的箱子就会产生混乱(运行时错误),并不知道这箱子里的东西是被偷了还是对方骗了你。

评论

  1. Little Vegetable Chicken
    5年前
    2019-11-07 19:11:44

    Oh, this is the most fantastic blog I’ve ever seen!

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇