Swift:解包的正确姿式

嗯,先来一段感慨

在掘金里面看见iOS各路大神各类底层与runtime,看得就算工做了好几年的我也一脸蒙圈,因而只好从简单的入手。程序员

文章最初发布在简书上面,有段时间了,考虑之后大部分时间都会在掘金学习,因而把文章搬过来了。稍微作了点润色与排版。web

对于Swift学习而言,可选类型Optional是永远绕不过的坎,特别是从OC刚刚转Swift的时候,可能就会被代码行间的?与!,有的时候甚至是??搞得稀里糊涂的。编程

这篇文章会给各位带来我对于可选类型的一些认识以及如何进行解包,其中会涉及到Swift中if let以及guard let的使用以及思考,还有涉及OC部分的nullablenonnull两个关键字,以及一点点对两种语言的思考。json

var num: Int? 它是什么类型?

在进行解包前,咱们先来理解一个概念,这样可能更有利于对于解包。安全

首先咱们来看看这样一段代码:markdown

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类型app

那么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的语言思考

我以前写过一篇文章,是说有关于一个字符串拼接函数的

从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卓越。

相关文章
相关标签/搜索