Swift:解包的正确姿势
嗯,先来一段感慨
在掘金里面看见iOS各路大神各种底层与runtime,看得就算工作了好几年的我也一脸蒙圈,于是只好从简单的入手。
文章最初发布在简书上面,有段时间了,考虑以后大部分时间都会在掘金学习,于是把文章搬过来了。稍微做了点润色与排版。
对于Swift学习而言,可选类型Optional是永远绕不过的坎,特别是从OC刚刚转Swift的时候,可能就会被代码行间的?与!,有的时候甚至是??搞得稀里糊涂的。
这篇文章会给各位带来我对于可选类型的一些认识以及如何进行解包,其中会涉及到Swift中if let以及guard let的使用以及思考,还有涉及OC部分的nullable和nonnull两个关键字,以及一点点对两种语言的思考。
var num: Int? 它是什么类型?
在进行解包前,我们先来理解一个概念,这样可能更有利于对于解包。
首先我们来看看这样一段代码:
var num: Int?
num = 10
if num is Optional<Int> {
print("它是Optional类型")
}else {
print("它是Int类型")
}
请先暂时不要把这段代码复制到Xcode中,先自问自答,num是什么类型,是Int类型吗?
好了,你可以将这段代码复制到Xcode里去了,然后在Xcode中的if上一定会出现这样一段话:
'is' test is always true
num不是Int类,它是Optional类型。
那么Optional类型是啥呢--可选类型,具体Optional是啥,Optional类型的本质实际上就是一个带有泛型参数的enum类型,各位去源码中仔细看看就能了解到,这个类型和Swift中的Result类有异曲同工之妙。
var num: Int?这是一个人Optional的声明,意思不是“我声明了一个Optional的Int值”,而是“我声明了一个Optional类型,它可能包含一个Int值,也可能什么都不包含”,也就是说实际上我们声明的是Optional类型,而不是声明了一个Int类型!
至于像Int!或者Int?这种写法,只是一种Optional类型的糖语法写法。
以此类推String?是什么类型,泛型T?是什么类型,答案各位心中已经明了吧。
正是因为num是一个可选类型。所以它才能赋值为nil, var num: Int = nil。
这样是不可能赋值成功的。因为Int类型中没有nil这个概念!
这就是Swift与OC一个很大区别,在OC中我们的对象都可以赋值为nil,而在Swift中,能赋值为nil只有Optional类型!
解包的基本思路,使用if let或者guard let,而非强制解包
我们先来看一个简单的需求,虽然这个需求在实际开发中意义不太大:
我们需要从网络请求获取到的一个人的身高(cm为单位)以除以100倍,以获取m为单位的结果然后将其结果进行返回。
设计思路:
由于实际网络请求中,后台可能会返回我们的身高为空(即nil),所以在转模型的时候我们不能定义Float类型,而是定义Float?便于接受数据。
如果身高为nil,那么nil除以100是没有意义的,在编译器中Float?除以100会直接报错,那么其返回值也应该为nil,所以函数的返回值也是Float?类型
那么函数应该设计成为这个样子是这样的:
func getHeight(_ height: Float?) -> Float?
如果一般解包的话,我们的函数实现大概会写成这样:
func getHeight(_ height: Float?) -> Float? {
if height != nil {
return height! / 100
}
return nil
}
使用!进行强制解包,然后进行运算。
我想说的是使用强制解包固然没有错,不过如果在实际开发中这个height参数可能还要其他用途,那么是不是每使用一次都要进行强制解包?
强制解包是一种很危险的行为,一旦解包失败,就有崩溃的可能,也许你会说这不是有if判断,然而实际开发中,情况往往比想的复杂的多。所以安全的解包行为应该是通过if let 或者guard let来进行。
func getHeight(_ height: Float?) -> Float? {
if let unwrapedHeight = height {
return unwrapedHeight / 100
}
return nil
}
或者:
func getHeight(_ height: Float?) -> Float? {
guard let unwrapedHeight = height else {
return nil
}
return unwrapedHeight / 100
}
那么if let和guard let 你更倾向使用哪个呢?
在本例子中,其实感觉二者的差别不大,不过我个人更倾向于使用guard let。
原因如下:
在使用if let的时候其大括号类中的情况才是正常情况,而外部主体是非正常情况的返回的nil;
而在使用guard let的时候,guard let else中的大括号是异常情况,而外部主体返回的是正常情况。
对于一个以返回结果为目的的函数,函数主体展示正常返回值,而将异常抛出在判断中,这样不仅逻辑更清晰,而且更加易于代码阅读。
解包深入
有这么一个需求,从本地路径获取一个json文件,最终将其转为字典,准备进行转模型操作。
在这个过程中我们大概有这么几个步骤:
1. 获取本地路径
func path(forResource name: String?, ofType ext: String?) -> String?
2. 将本地路径读取转为Data
init(contentsOf url: URL, options: Data.ReadingOptions = default) throws
3. JSON序列化
class func jsonObject(with data: Data, options opt: JSONSerialization.ReadingOptions = []) throws -> Any
4. 是否可以转为字典类型
我们可以看到以上几个函数中,获取路径获取返回的路径结果是一个可选类型而转Data的方法是抛出异常,JSON序列化也是抛出异常,至于最后一步的类型转换是使用as? [Sting: Any]这样的操作
这个函数我是这来进行设计与步骤分解的:
函数的返回类型为可选类型,因为下面的4步中都有可能失败进而返回nil。
虽然有人会说第一步获取本地路径,一定是本地有的才会进行读取操作,但是作为一个严谨操作,凡事和字符串打交道的书写都是有隐患的,所以我这里还是用了guard let进行守护。
这个函数看起来很不简洁,每一个guard let 后面都跟着一个异常返回,甚至不如使用if let看着简洁
但是这么写的好处是:在调试过程中你可以明确的知道自己哪一步出错
func getDictFromLocal() -> [String: Any]? {
/// 1 获取路径
guard let path = Bundle.main.path(forResource: "test", ofType:"json") else {
return nil
}
/// 2 获取json文件里面的内容
guard let jsonData = try? Data.init(contentsOf: URL.init(fileURLWithPath: path)) else {
return nil
}
/// 3 解析json内容
guard let json = try? JSONSerialization.jsonObject(with: jsonData, options:[]) else {
return nil
}
/// 4 将Any转为Dict
guard let dict = json as? [String: Any] else {
return nil
}
return dict
}
当然,如果你要追求简洁,这么写也未尝不可,一波流带走
func getDictFromLocal() -> [String: Any]? {
guard let path = Bundle.main.path(forResource: "test", ofType:"json"),
let jsonData = try? Data.init(contentsOf: URL.init(fileURLWithPath: path)),
let json = try? JSONSerialization.jsonObject(with: jsonData, options:[]),
let dict = json as? [String: Any] else {
return nil
}
return dict
}
guard let与if let不仅可以判断一个值的解包,而且可以进行连续操作
像下面这种写法,更加追求的是结果,对于一般的调试与学习,多几个guard let进行拆分,未尝不是好事。
至于哪种用法更适合,因人而异。
可选链的解包
至于可选链的解包是完全可以一步到位,假设我们有以下这个模型。
class Person {
var phone: Phone?
}
class Phone {
var number: String?
}
Person类中有一个手机对象属性,手机类中有个手机号属性,现在我们有位小明同学,我们想知道他的手机号。
小明他不一定有手机,可能有手机而手机并没有上手机号码。
let xiaoming = Person()
guard let number = xiaoming.phone?.number else {
return
}
print(number)
这里只是抛砖引玉,更长的可选链也可以一步到位,而不必一层层进行判断,因为可选链中一旦有某个链为nil,那么就会返回nil。
nullable和nonnull
我们先来看这两个函数,PHImageManager在OC与Swift中通过PHAsset实例获取图片的例子
[[PHImageManager defaultManager] requestImageForAsset:asset targetSize:size contentMode:PHImageContentModeDefault options:options resultHandler:^(UIImage * _Nullable result, NSDictionary * _Nullable info) {
//、 非空才进行操作 注意_Nullable,Swift中即为nil,注意判断
if (result) {
}
}];
PHImageManager.default().requestImage(for: asset, targetSize: size, contentMode: .default, options: options, resultHandler: { (result: UIImage?, info: [AnyHashable : Any]?) in
guard let image = result else { return }
})
在Swift中闭包返回的是两个可选类型,result: UIImage?与info: [AnyHashable : Any]?
而在OC中返回的类型是 UIImage * _Nullable result, NSDictionary * _Nullable info
注意观察OC中返回的类型UIImage * 后面使用了_Nullable来修饰,至于Nullable这个单词是什么意思,我想稍微有点英文基础的应该一看就懂--"可以为空",这不恰恰和Swift的可选类型呼应吗?
另外还有PHFetchResult遍历这个函数,我们再来看看在OC与Swift中的表达
PHFetchResult *fetchResult;
[fetchResult enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
let fetchResult: PHFetchResult
fetchResult.enumerateObjects({ (obj, index, stop) in
})
看见OC中Block中的回调使用了Nonnull来修饰,即不可能为空,不可能为nil,一定有值,对于使用这样的字符修饰的对象,我们就不必为其做健壮性判断了。
这也就是nullable与nonnull两个关键字出现的原因吧--与Swift做桥接使用以及显式的提醒对象的状态
一点点Swift与OC的语言思考
我之前写过一篇文章,是说有关于一个字符串拼接函数的
OC函数是这样的:
- (NSString *)stringByAppendingString:(NSString *)aString;
Swift中函数是这样的:
public mutating func append(_ other: String)
仅从API来看,OC的入参是很危险的,因为类型是NSString *
那么nil也可以传入其中,而传入nil的后果就是崩掉,我觉得对于这种传入参数为nil会崩掉的函数需要特别提醒一下,应该写成这样:
- (NSString *)stringByAppendingString:(NSString * _Nonnull)aString;
/// 或者下面这样
- (NSString *)stringByAppendingString:(nonnull NSString *)aString;
以便告诉程序员,入参不能为空,不能为空,不能为空,重要的事情说三遍!!!
反观Swift就不会出现这种情况,other后面的类型为String,而不是String?,说明入参是一个非可选类型。
基于以上对于代码的严谨性,所以我才更喜欢使用Swift进行编程。
当然,Swift的严谨使得它失去部分的灵活性,OC在灵活性上比Swift卓越。
作者:season_zhu
链接:https://juejin.cn/post/6931154052776460302