Objective-C 转 Swift 的第一道坎——论如何正确的处理可选类型

从 Objective-C 转 Swift 开发已经有一段时间了,这两门语言在总体的理念上差别仍是蛮大的。在这之中,可选类型的处理是每个使用 Swift 的开发者天天都要面临的问题,理解并正确处理好可选类型对于写出高质量的 Swift 代码和保证 iOS 项目的健壮性都是相当重要的。git

可选类型

要想处理好可选类型,就要先理解可选类型。github

一个可选类型表明有两种可能性:有一个值,你能够解包可选类型来访问该值;或者根本没有值。数据库

Objective-C 中不存在可选类型的概念,Objective-C 中最接近的东西就是 nil, nil 的意思是“没有有效的对象”。可是,这只适用于对象——它不适用于结构、基本数据类型或枚举值。对于这些类型,Objective-C 方法一般会返回一个特殊值(如 NSNotFound)来指示缺乏值。这种方法假设方法的调用者知道有一个特殊的值来测试,并记得检查它。Swift 的可选值可让你指出可能为 nil 的任何类型的值,而不须要特殊的常量。安全

例如,SwiftInt 类型有一个初始化方法,它试图将一个 String 值转换成一个 Int 值。可是,并非每一个字符串均可以转换成一个整数。字符串 "123" 能够转换为数字值 123,但字符串 "Hello, world" 没有一个明显的数值要转换。bash

下面的例子使用初始化方法来尝试将一个字符串转换为一个 Intapp

let possibleNumber = "123"
let convertedNumber = Int(possibleNumber)
// convertedNumber 被推断为 "Int?" 类型或 “可选的 Int”
复制代码

由于初始化方法可能会失败,因此它返回一个可选的 Int,而不是一个 Int。可选的 Int 被写为 Int?,而不是 Int。问号表示它所包含的值是可选的,这意味着它可能包含一个 Int 值,或者它可能根本不包含任何值。ide

nil

经过赋值给它一个特殊的值 nil 来设置一个可选变量为无值状态:性能

var serverResponseCode: Int? = 404
// serverResponseCode 包含一个实际的 Int 值为 404
serverResponseCode = nil
// serverResponseCode 如今不包含任何值 
复制代码

若是你定义了一个可选变量而不提供默认值,则该变量会自动设置为 nil测试

var surveyAnswer: String?
// surveyAnswer 自动设置为 nil
复制代码

SwiftnilObjective-C 中的 nil 不相同。在 Objective-C 中,nil 是一个指向不存在对象的指针。在 Swift 中,nil 不是一个指针,它是缺乏某种类型的值。任何类型的可选值均可以被设置为 nil,而不只仅是对象类型。fetch

处理可选类型

Swift 中,处理可选类型整体而言有五种方式:强制解包、可选绑定、隐式解包、Nil-Coalescing 运算符和可选链。接下来咱们将简要介绍一下这五种方式:

强制解包

一旦肯定可选值包含值,能够经过在可选值名称的末尾添加感叹号(!)来访问其内部值。这被称为强制解包一个可选的值。

print("convertedNumber has an integer value of \(convertedNumber!).")
复制代码

试着用 ! 访问不存在的可选值会触发运行时错误。在使用强制解包以前,必定要确保一个可选值不为 nil

if convertedNumber != nil {
    print("convertedNumber has an integer value of \(convertedNumber!).")
}
// 打印 "convertedNumber has an integer value of 123."
复制代码

可选绑定

你可使用可选绑定来发现可选值是否包含值,若是有,则使用该值用做临时常量或变量。可选绑定能够与 ifwhile 语句一块儿使用,以检查可选值内部的值,并将该值提取为常量或变量,做为单次操做的一部分。

使用 if 语句编写一个可选绑定,以下所示:

if let actualNumber = Int(possibleNumber) {
    print("\"\(possibleNumber)\" has an integer value of \(actualNumber)")
} else {
    print("\"\(possibleNumber)\" could not be converted to an integer")
}
// 打印 ""123" has an integer value of 123"
复制代码

若是转换成功,那么 actualNumber 常量能够在 if 语句的第一个分支中使用。它已经被初始化为包含在非可选的值中,因此没有必要使用 ! 后缀来访问它的值。

你可使用可选绑定的常量和变量。若是你想在 if 语句的第一个分支内操做 actualNumber 的值,你能够写 if var actualNumber,使得可选值做为一个变量而很是量。

你能够根据须要在单个 if 语句中包含尽量多的可选绑定和布尔条件,并用逗号分隔。若是可选绑定中的任何值为 nil,或者任何布尔条件的计算结果为 false,则整个 if 语句的条件被认为是错误的。如下 if 语句是等价的:

if let firstNumber = Int("4"), let secondNumber = Int("42"), firstNumber < secondNumber && secondNumber < 100 {
    print("\(firstNumber) < \(secondNumber) < 100")
}
// 打印 "4 < 42 < 100"
 
if let firstNumber = Int("4") {
    if let secondNumber = Int("42") {
        if firstNumber < secondNumber && secondNumber < 100 {
            print("\(firstNumber) < \(secondNumber) < 100")
        }
    }
}
// 打印 "4 < 42 < 100"
复制代码

隐式解包可选类型

有时从程序的结构中能够清楚的看到,在第一次设置值以后,可选值将始终有一个值。在这些状况下,每次访问时都不须要检查和解包可选值,由于能够安全地假定全部的时间都有一个值。

这些可选值被定义为隐式解包可选值。你写一个隐式解包的可选值,在你想要的可选类型以后放置一个感叹号(String!)而不是一个问号(String?

隐式解包可选值的背后是普通可选值,但也能够像非可选值同样使用,而没必要在每次访问时解包可选值。

let possibleString: String? = "An optional string."
let forcedString: String = possibleString! // 须要感叹号
 
let assumedString: String! = "An implicitly unwrapped optional string."
let implicitString: String = assumedString // 不须要感叹号
复制代码

若是隐式解包可选值为 nil,而且你尝试访问其包装的值,则会触发运行时错误。

你仍然能够对隐式解包可选值使用强制解包和可选绑定。

Nil-Coalescing 运算符

Nil-Coalescing 运算符(a ?? b)若是 a 包含一个值则解包它,或者返回一个默认值 b(若是 anil)。表达式 a 始终是可选的类型,表达式 b 必须匹配存储在 a 中的类型。

Nil-Coalescing 运算符是如下代码的简写:

a != nil ? a! : b
复制代码

上面的代码使用三元条件运算符,并强制解包(a!)来访问 a 来访问 a 不为 nil 时包装的值,不然返回 b。Nil-Coalescing 运算符提供了一种更简洁的方式来以简洁易懂的形式封装这个条件检查和解包。

若是 a 的值不是 nil,则不计算 b 的值。这就是所谓的短路计算。

可选链

可选链是查询和调用可能当前为 nil 的可选属性,方法和下标的过程。若是可选值包含一个值,那么属性,方法和下标调用将会成功;若是可选值为 nil,则属性,方法和下标调用返回 nil。多个查询能够连接在一块儿,若是链中的任何连接有一个为 nil,则整个连接将优雅的失败。

可选链能够做为强制解包的替代。

定义两个名为 PersonResidence 的类:

class Person {
    var residence: Residence?
}
 
class Residence {
    var numberOfRooms = 1
}
复制代码

建立一个新的 Person 示例,因为是它是可选类型,因此它的 residence 属性默认初始化为 nil

let john = Person()
复制代码

若是采用强制解包的方式访问 johnnumberOfRooms 属性,则会触发运行时错误:

let roomCount = john.residence!.numberOfRooms
// 这会触发运行时错误
复制代码

可选链提供了另外一种访问 numberOfRooms 值的方法。要使用可选链,请使用问号代替感叹号:

if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}
// 打印 "Unable to retrieve the number of rooms."
复制代码

可选链能够访问属性:

if let roomCount = john.residence?.numberOfRooms {
    print("John's residence has \(roomCount) room(s).")
} else {
    print("Unable to retrieve the number of rooms.")
}

john.residence?.numberOfRooms = 2
复制代码

可选链能够调用方法:

class Person {
    var residence: Residence?
}
 
class Residence {
    var numberOfRooms = 1
    func printNumberOfRooms () {
        print("John's residence has \(numberOfRooms) room(s).")
    }
}
...
john.residence?.printNumberOfRooms()
复制代码

可选链能够访问下标:

var testScores = ["Dave": [86, 82, 84], "Bev": [79, 94, 81]]
testScores["Dave"]?[0] = 91
testScores["Bev"]?[0] += 1
testScores["Brian"]?[0] = 72
// the "Dave" array is now [91, 82, 84] and the "Bev" array is now [80, 94, 81]
复制代码

就是这样。

Swift 中处理可选类型的建议

由于 Objective-C 中的 nil 对于开发者来讲是相对安全的,虽然向集合类型中添加 nil 会形成异常,可是对 nil 发送消息并不会有任何的问题(固然业务上可能会有问题)。但在 Swift 中,就像大多数其余语言同样,向 nil 发送消息会形成 crash。并且做为典型的现代强类型语言,可选类型的加入更是给以前长期使用 Objective-C 这种算是弱类型语言的 iOS 开发者带来了困扰。再此给开发者们一些处理可选类型的建议:

尽量避免声明可选类型的实例

除非一些必要的场景(例如代理模式,过程当中对象可能为 nil),尽量的使用非可选类型,包括但不限于属性声明和方法参数。

多使用可选绑定、Nil-Coalescing 运算符和可选链处理可选类型,避免使用强制解包和隐式解包

Swift 选用 “!” 做为强制解包和隐式解包的标志是有缘由的,这是在提醒咱们这是一种很危险的操做,每每会在乎想不到的时候给咱们的应用带来额外的 crash

不只要处理可选绑定和可选链的命中分支,对于 else 的状况也要进行额外状况的处理

在进行可选绑定时,可选类型不为 nil 的场景咱们都会进行处理,但每每会忽视 else 的状况,尽量也进行处理,那怕只是一句 log

进行可选绑定时,尽可能使用同名的局部变量

最佳实践是用同名的局部变量来可选绑定可选值,这样能够保证上下文清晰,不会由于出现了新的局部变量致使阅读代码的人反复对照。

if let serverResponseCode = serverResponseCode {
    print("serverResponseCode is (\serverResponseCode)")
} else {
    print("serverResponseCode is nil")
}
复制代码

至此,关于 Swift 中可选类型的处理就告一段落了,因为 Swift 是一门强类型的语言,若是有哪些场景是咱们处理的不正确的,编译器也会给出相应的提示,可是真正的危险可能不只止于此……

Objective-C 和 Swift 混编时如何正确的处理可选类型

除了一些在最近一段时间刚刚从零启动的项目,绝大多数的项目都是处于从 Objective-CSwift 代码过渡的阶段,这里面涉及到了对原有 Objective-C 代码进行可选非可选区分的问题。

Objective-C 中,你使用可能为 NULL 的原始指针(在 Objective-C 中称为 nil)来处理对象的引用。 在 Swift 中,全部值(包括结构和对象引用)都保证为非 nil 值。 相反,你表示能够经过将值的类型包装为可选类型表示其可能缺失。 当你须要表示缺乏某个值时,可使用值 nil

若是读过一些进行过适配 SwiftObjective-C 写的三方库的源代码以后会发现,不少都用到了这样的一对宏:

NS_ASSUME_NONNULL_BEGIN

...

NS_ASSUME_NONNULL_END
复制代码

这对宏的意思是,在这对宏之间声明的属性和方法,其中涉及到的类型都是非可选类型的。不少开发的同窗发现这样一种简单而又粗暴的将 Objective-C 一键适配到 Swift 的方法以后,果断的在全部的头文件中的开始和结尾处加上这对宏。而后悲剧就发生了,好比:

NS_ASSUME_NONNULL_BEGIN

// 若是设备的内存处于极小的状况下,会返回 nil
@property (nonatomic, strong) DataBase *dataBase;

NS_ASSUME_NONNULL_END
复制代码

在寻常的状况下调用数据库属性并不会有任何的问题,若是设备的内存处于极小的状况下,会返回 nil,这在纯 Objective-C 的代码中也不会有什么问题,可是当混编时:

let x = object.dataBase().fetchUserInfo() // 当 dataBase 返回 nil 时,会 crash。
复制代码

由于你已经经过宏声明了 dataBase 属性是非可选的,因此编译器就会认为这个属性是非可选的,不会给出任何处理可选的提示。

看到这里你可能会说,对于这类状况,能够经过判断是否为 nil 来进行处理,好比:

if object.dataBase() != nil {
    ...
}
复制代码

这在 debug 模式下是行的通的,可是在 release 模式下,iOS 系统为了优化性能,会对全部标记了非可选类型的对象的与 nil 的比较直接认为是 true,直接落入了括号中,形成更不可查的 crash。因此最好的处理方式是对任何可能出现 nil 可能的属性或方法参数都加上 nullable

@property (nonatomic, strong, nullable) DataBase *dataBase;
复制代码

这样就能够通知编译器这是一个可选类型属性,该有的一些提示和处理也会由编译器来提供。从而避免了 release 以后出现线上 crash 的悲剧。

原文地址:Objective-C 转 Swift 的第一道坎——论如何正确的处理可选类型

若是以为我写的还不错,请关注个人微博@小橘爷,最新文章即时推送~

相关文章
相关标签/搜索