极速 Swift 教程之十 | 可选型

欢迎关注微信公众号「Swift 花园」

处理缺失的数据

我们已经会使用 Int 这样的类型来存储像 5 这样的数值。不过,当你想要存储用户年龄这样的属性,并且你还不知道该用户的年龄时你该怎么做呢?

你可能会说,“我可以暂时存成 0”,但这样一来你就会混淆新生儿和你不知道年龄的用户。你应该用一个特殊的数字,比如 1000 或者 -1 来代表 “未知”,这两个数字都不可能是年龄,但你能记得住这些特殊数字的含义吗?

Swift 的解决方案称为 optional ,即可选型。你可以基于任意类型创建可选型。一个可选型整数可以有诸如 1 或者 1000 这样的数字,也可能没有任何值,即值可以缺失, Swift 里表示缺失是用 nil

为了把一个类型变成可选型,只需要在类型后面加一个问号。举个例子,我们可以这样把一个整数变成可选型:

1
var age: Int? = nil

这个可选型现在并不保有任何整数,但是如果之后我们知道了年龄值,我们可以给它重新赋值。

1
age = 38

解包可选型

可选型字符串可能包含一个 “Hello” 或者 nil

考虑这样一个可选字符串:

1
var name: String? = nil

当我们调用 name.count 时会发生什么呢?一个真的字符串有 count 属性,它存储字符串的字符数量。但它是 nil 时,并没有 count 属性。

因此,试图直接使用 name.count 是不安全的, Swift 也不允许。取而代之的是,我们必须先检查可选型里面有些什么,这个过程被称为 解包

解包可选型的一种常见做法是采用 if let 语法,它是一种有条件的解包。如果可选型里有值,你就可以使用它,如果没有则条件失败。

举个例子:

1
2
3
4
5
if let unwrapped = name {
print("\(unwrapped.count) letters")
} else {
print("Missing name.")
}

如果 name 持有一个字符串,这个字符串会被放进一个叫 unwrapped 的常规字符串,然后我们就可以读取它的 count
属性。另一种情况,如果 name 是 nil, 那么 else 中的代码将被执行。


用 guard 解包可选型

if let 的一个替代方案是 guard let ,后者也可以用来解包可选型。 guard let 会为你解包,但是当它发现可选型里是 nil 时会期望你退出它所处的函数,循环或者条件。

因此, if letguard let 的主要区别在于 guard let 之后可选型还可以继续使用。

让我们尝试一下 greet () 函数。它将接收一个可选字符串作为唯一的参数,当它解包发现这个参数是 nil 时会打印消息并且退出函数。因为可选型 unwrappedguard let 的语句块结束之后作用,我们可以在函数最后打印这个解包后的字符串。

1
2
3
4
5
6
7
func greet(_ name: String?) {
guard let unwrapped = name else {
print("You didn't provide a name!")
return
}
print("Hello, \(unwrapped)!")
}

通过使用 guard let ,你可以在函数的前面解决可选型的解包问题,在这之后的代码就可以愉快地使用解包后的值而不必担心出错了。


强制解包

可选型代表可能存在也可能不存在的数据,而某些时候你是 确切 知道一个可选型的值并不是 nil 。在这种情况下, Swift 允许你强制解包这个可选型:通过把一个可选型转换为非可选型的方式。

举个例子,下面你试图将一个字符串转换成 Int ,就像这样:

1
2
let str = "5"
let num = Int(str)

上面的代码中 num 其实是一个可选型 Int 。因为你有可能尝试转换一个不是数字的字符串导致转换失败。

尽管 Swift 不确定转换能否成功,你自己是知道你的结果是否可以安全地执行强制解包。这个强制解包是通过在 Int (str) 之后添加 ! 来完成的。

1
let num = Int(str)!

通过强制解包, Swift 将立刻解包 num ,把它变成一个常规的 Int 而不是 Int? 。但是假如你是错的,比方说 str 是某个不能转换成数字的字符串,那么你的代码将崩溃。

强制解包的操作符常常被戏称为崩溃操作符。因此,只有当你确信强制解包是安全的你才能这么做。


隐式解包可选型

像常规可选型一样,隐式解包可选型可能包含一个值也可能包含 nil 。不过,跟常规可选型不一样的是,隐式解包可选型时不需要再解包,你可以把它们当做非可选型一样使用。

隐式解包可选型是通过类型名之后添加感叹号来声明的,就像这样:

1
let age: Int! = nil

由于它们表现起来就像已经被解包过的样子,你并不需要 if let 或者 guard let 这样的代码来测试它们。不过,当你试图使用它们而它们实际上没有值时,即它们的值是 nil 时,你的代码将崩溃。

隐式解包可选型存在的意义在于,有些情况某个变量一开始时是 nil ,但当你需要用到它之前它总会有值。如果你可以确信这一点,那么采用隐式解包可选型可以省去一直写 if let 的麻烦。

值得一提的是,如果条件允许采用常规可选型,安全起见最好总是采用常规可选型。


空合运算符

空合运算符解包一个可选型,如果可选型包含值则返回这个值,如果可选型不包含值,即可选型的值是 nil ,那么返回某个默认值。两种情况,返回值都不再是可选型:它要么是可选型里包含的有效值,要么是一个备选的默认值。

下面是一个接收整数作为唯一参数并且返回可选型字符串的函数:

1
2
3
4
5
6
7
func username(for id: Int) -> String? {
if id == 1 {
return "Taylor Swift"
} else {
return nil
}
}

如果我们以 ID 15 来调用这个函数,我们将得到 nil 。通过空合运算符,我们可以提供一个叫 “Anonymous” 的默认值,就像这样:

1
let user = username (for: 15) ?? "Anonymous"

它将检查 username () 函数返回的值:如果是一个字符串,它将被解包并放入 user ,如果是 nil ,则使用 “Anonymous” 替代。


可选链

在使用可选型时, Swift 为我们提供了一种快捷方式:假如你试图访问形如 a.b.c 这样的代码并且 b 是可选型,你可以在 b 后面写一个问号来启用 可选链a.b?.c

当代码运行时, Swift 会检查 b 是否有值,如果它是 nil ,那么这行代码剩下的部分将被忽略。Swift 会立即返回 nil 。但是如果 b 有值,它将被解包,代码执行将继续。

下面是一个名字的数组:

1
let names = ["John", "Paul", "George", "Ringo"]

我们将使用数组的 first 属性,如果数组里第一个名字有值的话返回这个名字,如果数组为空则返回 nil 。在结果之上,我们调用 uppercased () 把它变成一个全大写的字符串:

1
let beatle = names.first?.uppercased ()

上面的问号就是可选链。如果 first 返回 nil ,那么 Swift 就不会尝试将它转换成全大写,它会将 beatle 立即设置为 nil


可选型 try

让我们回忆一下可能抛出错误的函数那一节的知识,看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
enum PasswordError: Error {
case obvious
}

func checkPassword(_ password: String) throws -> Bool {
if password == "password" {
throw PasswordError.obvious
}

return true
}

do {
try checkPassword ("password")
print("That password is good!")
} catch {
print("You can't use that password.")
}

通过 dotrycatch ,我们得以运行可能抛出错误的函数,并且优雅地处理错误。

上面的 try 写法其实有另外两种选择,这两种选项都能加深你对可选型和强制解包的理解。

第一个是 try? ,它将可能抛出错误的函数转换成返回可选型的函数。如果函数抛出错误,那你就会得到 nil 作为函数的执行结果,否则你会得到将返回值包装之后的可选型。

尝试使用 try? 来执行 checkPassword () ,像下面这样:

1
2
3
4
5
if let result = try? checkPassword ("password") {
print("Result was \(result)")
} else {
print("D'oh.")
}

另外一种选择是 try! ,当你确信函数一定不会失败时你可以采用它。如果函数实际抛出了错误,你的代码将崩溃。

使用 try! 来重写前面的代码:

1
2
try! checkPassword ("sekrit")
print("OK!")

失败构造器

当我们说到强制解包的时候,我用了下面的代码:

1
2
let str = "5"
let num = Int(str)

它将一个字符串转换成一个整数,但由于你可以传入任意字符串,你得到的实际上是一个可选型整数。

这里用到一种叫做 失败构造器 的东西:它是一种可能成功也可能失败的构造器。你在结构体或者类里面用 init?() 来实现失败构造器。如果某些东西出错,它将返回 nil 。因此这种构造器返回的是某种类型的可选型,你用之前需要解包。

举个例子,我们现在要求 Person 结构体必须通过一个 9 字符的 ID 字符串来构造。只要不是 9 个字符,都会返回 nil

Swift 代码如下:

1
2
3
4
5
6
7
8
9
10
11
struct Person {
var id: String

init?(id: String) {
if id.count == 9 {
self.id = id
} else {
return nil
}
}
}

类型转换

Swift 知道每个变量的类型,但有的时候你知道的信息比 Swift 更多。举个例子,这里有三个类:

1
2
3
4
5
6
7
class Animal { }
class Fish: Animal { }
class Dog: Animal {
func makeNoise() {
print("Woof!")
}
}

我们创建几个 Fish 和几个 Dog ,然后把它们放进一个数组,像下面这样:

1
let pets = [Fish(), Dog(), Fish(), Dog()]

Swift 知道 FishDog 都继承自 Animal 类,因此它通过类型推断将 pets 创建为一个 Animal 类型的数组。

如果我们想遍历 pets 数组,让所有的狗发出叫声,我们需要执行一次类型转换: Swift 将检查每个 pet 是否 Dog 对象,以便我们调用 makeNoise () 方法。

这里用到了一个关键字 as? ,它将返回一个可选型:类型转换失败时返回 nil ,成功则返回转换后的类型。

Swift 代码如下:

1
2
3
4
5
for pet in pets {
if let dog = pet as? Dog {
dog.makeNoise ()
}
}

总结

让我们来总结一下。

  1. 可选型让我们可以用一种清晰无歧义的方式表示值缺失的情况。
  2. Swift 不允许不经解包就使用可选型,解包可以用 if let 或者 guard let
  3. 你可以用感叹号强制解包可选型,不过如果解出来的是 nil ,你的代码将会崩溃。
  4. 隐式解包可选型没有做常规可选型的安全性检查。
  5. 你可以使用空合运算符解包一个可选型,以便可选型里没有值时提供一个默认值。
  6. 可选链用于操作可选型,如果可选型的值是空的,后续代码将被忽略。
  7. 你可以使用 try? 将一个可能抛出错误的函数转换成一个可选型的返回值,或者使用 try! 在抛出错误时崩溃。
  8. 如果你需要在输入不合理时让构造过程失败,可以使用 init?() 来创建失败构造器。
  9. 你可以使用类型转换将一个类型转换为另一个类型。

Linkedin
Plus
Share
Class
Send
Send
Pin