Day12 – 可选

Swift —— 100 天从新手到大师
它的目标是那些想要学习如何构建真正的 iOS 应用程序的开发者
如果说的是你 —— 准备好了吗


关于Optional 你需要知道这些

在 




解包 Nil 可选项

 



一文中,我们介绍 Swift 中的一种崩溃现象——强制解包 nil 可选项。

在运行下面代码时,应用程序发生崩溃

var name: String? = ""


print(name!.count) //0


name = nil


print(name!.count) // Fatal error: Unexpectedly found nil while unwrapping an Optional value

今天我们将完结关于 iOS 基础知识的学习。我们来解决这一个困扰着 Swift 开发者的一点——可选项。
我们将了解它是什么,它为什么有用,以及如何使用它来使代码更安全、更少的 Bug、更易于维护。

Swift 中的

nil

在 Swift 中用 

nil
 来表示一个 


不存在的值



Null references

在 1965 年,

Tony Hoare 爵士
 在编程语言中引入了这个被其称之为

一个价值10亿美元的错误
 的空指针。从此,在大多数的较老的命令语言(C、C++、java等)中,如何写出更安全的代码,都在令程序员

欲仙欲死
 ,为了避免触发


NullPointerException
这种错误与语言中的大多数其他类型错误不同,它在编译期间没有被捕获。某些语言可能会进行一些检查并发出警告,但不会提示这是个错误。
为了避免这种错误,从此,程序员开始笔耕不辍的在为自己的每个调用都加一个

if (x != nil) {


}

以期让自己的每次调用都变得安全。但是,即便如此,我们依然还是小心翼翼,因为一旦我们遇到 

NullPointerException
 错误,还是需要花费大量时间去找到那个错误开始的地方。

哨值


表示特殊条件( 如空值,0
)的值被称为 



哨值


, 例如当我们判断一个字符串是否可用时,我们判断当前字符串的长度是否大于 0,当我们不知道值为什么时, 我们一般将变量的初始值定义成一个
 



哨值



var string: String = ""


其他编程语言中,空值通常是 
哨值

 概念的延伸,例如,iOS 开发者所熟悉的 Objective-C 中的

nil

的概念。Objective-C 中的  nil

 表示当前所  分配

 的  NSObject

 的内容被设置为  zero
 。



在 Swift 中提供了一种相当优雅的方式将


不存在的值

的概念直接整合到语言中。Swift 提供了一个新的类型, 可选类型,来说明

要么有一个 T 类型的值要么什么都没有


Optional

当使用可选值时,都会使用可选类型,即便我们从来没有用过 

Optional
。Swift 的类型系统通常用尾随的问号 

?
来简写 

Optional
,而不是显示完整的类型名。


let shortForm: Int? = Int("42")
let longForm: Optional<Int> = Int("42")

Optional 的本质上就是一个


包含两种情况的枚举


enum Optional<Wrapped> : ExpressibleByNilLiteral  {
case none
case some(Wrapped)
}

所以对一个可选值进行赋值时,也可以使用

var name: String? = ""
name = Optional.none
if name == nil {
print("Optional.none 和 nil 等值")
}

当我们实现一个接受  Optional 参数的方法时,除了判断该值是否为空,我们也可以利用枚举判断来进行优化

func printName(_ name: String?) {
switch name {
case .some(let unwrappedValue):
print("Name is \(unwrappedValue)")
case .none:
print("Name is nil")
}
}

Optional 是一个遵守 

ExpressibleByNilLiteral
 协议的的一种枚举, 官方文档中不建议我们让其他 


哨值 

通过协议一致性来支持这个 

ExpressibleByNilLiteral
 协议。


我们仿照 Optional 的实现,来简单实现一个自己的 MyTestOptional 枚举:

enum MyTestOptional<Wrapped> : ExpressibleByNilLiteral {
init(nilLiteral: ()) {
print("要成为 nil 了")
self = .none
}
init(_ some: Wrapped) {
print("我被重新赋值了 \(some)")
self = .some(some)
}


case none


case some(Wrapped)
}


var test = MyTestOptional.some(Int("42"))
test = nil
test = MyTestOptional.init(Int("42"))


var testOptional: MyTestOptional<Int> = MyTestOptional.some(Int("42")!)
testOptional = nil

通过查看文档我们还发现 Optional 还支持一些更加便捷的高阶函数。

map()
 和

flatMap()

let sideLength: Int? = Int("20")
let possibleSquare = sideLength.map { $0 * $0 }
print(possibleSquare) // Prints: "Optional(400)"


var name: String? = "Antoine van der Lee"
let validName = name.flatMap { name -> String? in
guard name.count > 5 else { return nil }
return name
}
print(validName) // Prints: "Optional("Antoine van der Lee")"

解包可选项

Swift 不会让我们在不解包的情况下使用可选项的值。在使用之前,我们需要解包可选项。

文章开头,介绍了



强制解包



可选项会引起崩溃





那么什么是强制解包?我们应该如何安全的解包可选项呢?下面我们一一介绍。

强制解包(forced unwrapping)

可选项类型表明值要么有要么没有。Swift 允许开发者强制解包可选项,当我们能确定当前值不为 

nil
 时,将可选类型转换为非可选类型。

let num = Int("5")
let unwarppedNum = num!

只有当你确定当前值不为 

nil
 时,你才可以进行强制解包可选项 —— 否则应用程序将发生崩溃。

我们需要一些安全的解包方法。一个安全的可选项解包需要满足以下两个条件



  • 取值


    当我们需要使用可选项值的时候,可以提取当前值



  • 检查可用性


     当可选项值为 

    nil
    ,可以不处理

可选绑定(optional binding)

if let
 和  

guard let
 通常被称作 




可选绑定(optional binding)




,我们将详细介绍这两种方法,并引申他们的变种。



if let

解包可选项的最常见的方法是使用 

if let
 语法,它是使用条件进行解包。如果可选项是有值的,那么我们可以在条件语句中使用,否则,条件失败。

var name: String? = nil


if let unwrapped = name {
print("\(unwrapped.count) letters")
} else {
print("Missing name.")
}


guard let

Swift 为我们提供了一种替代 

if let
 的方法,称为 

guard let
。同样可以完成对可选项的检查和取值。通常,

guard let
 用于当检查失败时退出当前函数、循环或条件,它解包后的值将在检查之后继续保留。


func double(number: Int?) -> Int? {
guard let number = number else {
return nil
}
return number * 2
}
let input = 5
if let doubled = double(number: input) {
print("\(input) doubled is \(doubled).")
}


if var



guard var

if var
和 

guard var
 算是  

if let
 和 

guard let
 的变种,如果我们需要在方法中修改可选项的值或其某个属性,用户后期处理某些东西,我们可以使用 

if var
和 

guard var




var userName: String? = "需要点赞"


if var unwrapped = userName {
print("\(unwrapped.count) letters")
unwrapped = "iOS 成长指北"
} else {
print("Missing name.")
}


print(userName ?? "")


func helloToUser(_ name: String?) {
guard var unwrapped = name else {
return
}
unwrapped = "iOS 成长指北"
print("需要点赞\(unwrapped)")
}
print(userName ?? "")

不过,请注意,所做的更改不会反映回原始可选项。

隐式解析可选类型(Implicitly unwrapped optionals)

这可能是解包可选项中最复杂的一种概念。

var implicitly: String! = nil
implicitly = "iOS 成长指北"
print(implicitly.count)

当我们明确知道有一个值,在其被赋值以后,其永远不会在被赋值成为 

nil
 。如果,每次调用这个值,为了安全都进行一次可选绑定的话,这样的代码,冗余且低效。但是如果我们每次都进行强制解包的话,那么每次调用的时候,都需要在其后面跟一个

!


当可选类型被第一次赋值之后就可以确定之后一直有值的时候,我们可以使用隐式解析可选类型来解决这个问题。我们可以把想要用作可选的类型的后面的 

?
改成 

!
 来声明一个隐式解析可选类型。

一个常见的现象就是,当我们进行 Xib 进行界面绘制时,拖拽链接一个界面的组件,比如, 按钮,页面自动生成对应的代码。

@IBOutlet weak var button: UIButton!

当页面未加载时,

button
 为 

nil
, 一旦页面加载完成,

button
 将跟随着页面的生命周期,几乎不会再被重置为 

nil
。所以当我们在页面中使用 

button
 并为其赋值时,可以认为 

button
 不会为 

nil






无论是可选项还是隐式解析可选类型,都是可选类型的一种。所以都可以使用可选绑定。
在我们手写页面组件元素时,我们应该如何写呢?当我们涉及页面时,会一一介绍。

nil 合并运算符

理论上来说,严格意义上 

nil
 合并运算符并不能算是解包可选项的一种,但是确实我们常用的处理可选项的方法。

let name: String? = "iOS 成长指北"
print(name?.count ?? 0)

使用 

??
 为可选项值为 

nil
时,提供一个哨值。避免后面的方法使用了非可选项的值。

拓展可选

我们知道,Optional 的本质是一个遵循 

ExpressibleByNilLiteral
 协议的枚举。我们在拓展中提到过,我们可以为Optional 提供一个拓展。

extension Optional where Wrapped == String {
var orEmpty: String {
return self ?? "iOS 成长指北"
}
}


var whereName: String? = "需要点赞"
print(whereName.orEmpty) // Prints: "Antoine van der Lee"
whereName = nil
print(whereName.orEmpty) // Prints: ""

当我们使用使用字符串的值或方法时,我们可以使用 

orEmpty
 方法,不过前提是,定义的哨值不要成为自己的 


累赘



可选链(Optional Chaining)

Swift 的可选链允许我们在一行代码中使用多个可选项,如果其中任何一个值是 

nil
,那么当前语句的返回值就为 

nil
。通过这种链式调用,我们可以在当前值可能为 

nil
 的可选项上请求和调用属性、方法及下标的方法。


属性

我们可以使用 




可选链

 来获取或设置属性。


class DriversLicence {
var pointsOnLicence = 0
}


class Person {
var licence : DriversLicence?
}


var tom: Person? = nil


print(tom?.licence?.pointsOnLicence ?? 0) // Prints: 0


tom?.licence?.pointsOnLicence = 10


print(tom?.licence?.pointsOnLicence ?? 0) // Prints: 0

当我们存在一个可选对象,并且其对象属性为一个可选属性时,我们可以通过使用可选链来进行读取和赋值。

可选绑定

当我们在使用




可选链 




来获取属性时,推荐和可选绑定一起使用

if let point = tom?.licence?.pointsOnLicence {
print("\(point)")
}


func testPerson(_ person: Person?) {
guard let point = person?.licence?.pointsOnLicence else {
return
}
print("\(point)")
}

访问下标

使用可选链访问下标包括两种,一种是下标方法返回的是可选项,例如,字典获取某一个 key 值,另外一种是取下标的对象就是一个可选项,比如这个字典就是可选的。

var info = ["fans" : (min:10, max:10000000)]


if let number = info["fans"]?.min {
print("The number of 公众号:iOS成长指北 \(number)")
}

之所以分成两种类型,其实就是一个关于 

?
 在前在后的问题。

var info: Dictionary? = ["fans" : (min:10, max:10000000)]


if let number = info?["fans"]?.min {
print("The number of 公众号:iOS成长指北 \(number)")
}

可选绑定

在可选绑定一节,我们介绍了关于使用可选绑定进行赋值的操作,只能在作用域之内用。但是如果是下标的可选链访问,我们能不能进行赋值操作呢?

if let number = info?["fans"]?.min = 100 {
print(type(of: number)) // ()
}
print(info as Any) //Prints:Optional(["fans": (min: 100, max: 10000000)])

当通过可选链设置一个属性时,返回一个可选的元组(

Void?
 或 

()?


方法

可以通过可选链式调用来调用方法,并判断是否调用成功,即使这个方法 


没有返回值


我们拓展一下之前的例子,然后我们来看看我们如何使用可选链和可选绑定。

当有返回值的时候,绑定获取的值,否则绑定一个默认的返回值 

()
,我们知道一个没有返回值的函数它的类型是 

() -> ()


class DriversLicence {
var pointsOnLicence = 0

func changePointsOnLicence() {
self.pointsOnLicence = 2
}
}


class Person {
var licence : DriversLicence?
}


var tom: Person? = Person()
tom?.licence = DriversLicence()
if let point = tom?.licence?.changePointsOnLicence() {
print(type(of: point)) //Prints:()
}

可选协议方法

如果你有 Objective-C 的经验,你可能会错过可选的协议方法。虽然 Swift 中有更好的方法来编写可选协议方法,但标准库中最常这么操作:

@objc protocol UITableViewDataSource : NSObjectProtocol {


@objc optional func numberOfSections(in tableView: UITableView) -> Int


// ...
}


let tableView = UITableView()
let numberOfSections = tableView.dataSource?.numberOfSections?(in: tableView)

多级可选链

可以通过连接多个可选链式调用在更深的模型层级中访问属性、方法以及下标。

关于多级可选链我们需要知道的是,可选链的返回值为要么确定的值,要么是

nil
,所以使用多级可选链返回的是一个

*?
而不是

*??


常见用法

除了使用可选项来表明的对象的值不存在以外,我们还有一些常见的其他用法

将错误转换成可选值(Optional try)

可以使用 

try?
 通过将错误转换成一个可选值来处理错误。如果是在计算 

try?
 表达式时抛出错误,该表达式的结果就为 

nil



当我们使用一个异常捕捉的方法时,我们一般这么使用

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.")
}

当是我们可以将抛出的错误转换成可选值,我们可以使用

if let result = try? checkPassword("password") {
print("Result was \(result)")
}else {
print("You can't use that password.")
}

Objective-C 开发者在开发过中很少用到 


try catch

 语句。也许,使用 

try?
 更加符合你的习惯。

与 

try?
 对应的就是 

try!
,如果你确定你的异常一定不会出现,你可以通过这个方式,否则就如同强制解包一样,一旦异常发生,就会发生崩溃。

可能这不失为一个测试手段,但是绝对不要在生产环境使用。毕竟崩溃比错误更可怕

可失败构造器(Failable initializers)

在协议与拓展一章节中,我们建议在协议中如果要在协议中声明构造器,建议声明一个


可失败构造器


理论上来说,一个合格的构造器在运行结束以后应该生成一个实例。但是如果我们有什么值使我们不可以没有的,或者我们的对象中某个值一定要满足某个条件,例如我们的身份证。那么我们可以初始化一个


可失败构造器(Failable initializers)

,返回值为

nil

struct Person {
var id: String


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


}

类型转换(Typecasting)

在 Swift 编程过程中,你需要时刻保持对类型的敏感度。你必须明确你变量的类型。
我们知道 Swift 中存在继承,也存在基于某个协议来作为数组、字典或其他容器中的元素类型。
我们定义一个数组

class Animal { }
class Fish: Animal { }


class Dog: Animal {
func makeNoise() {
print("Woof!")
}
}


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

不同的类中实现了不同的方法
如果我们有一个循环来遍历我们数组中的对象,并且针对不同类型,调用不同方法。


as?

同样可以用在可选绑定中。

for pet in pets {
if let dog = pet as? Dog {
dog.makeNoise()
}
}

当在 Swift 中输入强制转换值时,你可以检查该值的类型,并且可以在其自己的类层次结构中将其视为不同的类型。通常在


子类和超类之间




相关类型之间

进行强制转换。

其他用法

我们再介绍一些其他的用法,这些用法可能不那么的常见

嵌套可选(Nested optionals)

在多级可选链中我们提到一个

*??
 的概念。对一个可选值进行嵌套解包。虽然多级可选链并不会生成一个嵌套可选,但是在实际工作过程中,还是可能会写出嵌套可选,使用嵌套可选时需要实现嵌套解包。

嵌套可选是一个历史问题,具体我们可以查看 

SE-0230 – Flatten nested optionals resulting from ‘try?’
 。

当我们这么写的话

let nameAndAges: [String:Int?] = ["Antoine van der Lee": 28]
let antoinesAge = nameAndAges["Antoine van der Lee"]
print(antoinesAge as Any) // Prints: "Optional(Optional(28))"
print(antoinesAge! as Any) // Prints: "Optional(28)"
print(antoinesAge!!) // Prints: "28"
print(type(of: antoinesAge)) // Prints: Optional<Optional>

一定要相当注意我们关于可选的实现,否则取值时会出现问题。

XCTUnwrap 编写一个可选的单元测试

当编写单元测试的时候,有一个很好的方法来处理可选项——而不是使用强制解包。
你可以使用 XCTUnwrap,如果可选参数不包含值,它会抛出错误:

func testBlogPostTitle() throws {
let blogPost: BlogPost? = fetchSampleBlogPost()
let unwrappedTitle = try XCTUnwrap(blogPost?.title, "Title should be set")
XCTAssertEqual(unwrappedTitle, "Learning everything about optionals")
}

map,compactMap 和 flatMap

使用转换将值集合映射到新值数组中。Swift 标准库为这种映射提供了三个主要 api——map、flatMap 和 compactMap。
我们一般这么写来获取一个请求

func makeRequest(forURLString string: String) -> URLRequest? {
guard let url = URL(string: string) else {
return nil
}


return URLRequest(url: url)
}

但是,其实我们也可以使用

map
 方法

func makeRequest(forURLString string: String) -> URLRequest? {
URL(string: string).map { URLRequest(url: $0) }
}

善用枚举

当我们使用一个存在可选的枚举值,我们处理可枚举的值的时候,我们可以

enum Relationship {
case friend
case family
case coworker
}


let relationship: Relationship? = nil


switch relationship {
case .some(let relationship):
switch relationship {
case .friend:
print("friend")
case .family:
print("family")
case .coworker:
print("coworker")
}
case .none:
print("nil")
}

但是我们可以用一个 

case nil
 来处理这个

switch relationship {
case .friend:
print("friend")
case .family:
print("family")
case .coworker:
print("coworker")
case nil:
print("nil")
}

参考资料

Apple Developer 的 Optional
100 Days of Swift – Optionals
Optional Chaining in Swift
Optionals
Optionals in Swift explained: 5 things you should know

本文的提纲来自于 hackingwithswift 的 《100 Days of Swift》,然后根据每一个知识点进行总结

欢迎点赞、转发、评论、在看。
如果有任何问题,欢迎留言交流