作者 | 启明星小组
上一篇咱们介绍了挪动开发常见的内存透露问题,见《百度工程师挪动开发避坑指南——内存透露篇》。本篇咱们将介绍Swift语言局部常见问题。
对于Swift开发者,Swift较于OC一个很大的不同就是引入了可选类型(Optional),刚接触Swift的开发者很容易在相干代码上踩坑。
本期咱们带来与Swift可选类型相干的几个避坑指南:可选类型要判空;防止应用隐式解包可选类型;正当应用Objective-C标识符;审慎应用强制类型转换。心愿能对Swift开发者有所帮忙。
一、可选类型(Optional)要判空
在Objective-C中,能够应用nil来示意对象为空,然而应用一个为nil的对象通常是不平安的,如果应用不慎会呈现解体或者其它异样问题。在Swift中,开发者能够应用可选类型示意变量有值或者没有值,能够更加清晰的表白类型是否能够平安的应用。如果一个变量可能为空,那么在申明时能够应用?来示意,应用前须要进行解包。例如:
var optionalString: String?
在应用可选类型对象时,须要进行解包操作,有两种解包形式:强制解包与可选绑定。
强制解包应用 ! 润饰一个可选对象 ,相当于通知编译器『我晓得这是一个可选类型,但在这里我能够保障他不为空,编译时请疏忽此处的可空校验』,例如:
let unwrappedString: String = optionalString! // 运行时报错:Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
这里应用 ! 进行了强制解包,如果optionalString为nil,将会产生运行时谬误,产生解体。因而,在应用 ! 进行强制解包时,必须保障变量不为nil,要对变量进行判空解决,如下:
if optionalString != nil { let unwrappedString = optionalString!}
相较于强制解包的不安全性,一般而言举荐另一种解包形式,即可选绑定。例如:
if let optionalString = optionalString { // 这里optionalString不为nil,是曾经解包后的类型,能够间接应用}
综上,在对可选类型进行解包时应尽量避免应用强制解包,采纳可选绑定代替。如果肯定要应用强制解包,那么必须在逻辑上齐全保障类型不为空,并且做好正文工作,以减少后续代码的可维护性。
二、防止应用隐式解包可选类型(Implicitly Unwrapped Optionals)
因为可选类型每次应用之前都须要进行显式解包操作,有时变量在第一次赋值之后,就会始终有值,如果每次应用都显式解包,显得繁琐,Swift引入了隐式解包可选类型,隐式解包可选类型能够应用 ! 来示意,并且应用时不须要显式解包,能够间接应用,例如:
var implicitlyUnwrappedOptionalString: String! = "implicitlyUnwrappedOptionalString"var implicitlyString: String = implicitlyUnwrappedOptionalString
上述例子的隐式解包,在编译和运行过程中都不会产生问题,但如果在两行代码两头插入一行 implicitlyUnwrappedOptionalString = nil将会产生运行时谬误,产生解体。
在咱们理论我的项目中,一个模块通常由多人保护,通常很难保障变量在第一次赋值之后始终不为nil或者只有在第一次正确赋值之后应用,从平安角度思考,在应用隐式解包类型之前也要进行判空操作,但这样就和应用可选类型没有区别。对于可选类型(?),不通过解包间接应用编译器会报告谬误,对于隐式解包类型,则可间接应用,编译器无奈帮忙咱们做出是否为空的查看。因而,在理论我的项目中,不举荐应用隐式解包可选类型,如果一个变量是非空的,则抉择非空类型,如果不能保障是非空的,则抉择应用可选类型。
三、正当应用Objective-C标识符
与Swift不同的是,OC是一种动静类型语言,对于OC而言没有optional这个概念,无奈在编译期间查看对象是否可空。苹果在 Xcode 6.3 中引入了一个 Objective-C 的新个性:Nullability Annotations,容许编码时应用nonnull、nullable、null\_unspecified等标识符通知编译器对象是否是可空或者非空的,各标识符含意如下:
nonnull,示意对象是非空的,有\_\_nonnull和\_Nonnull等价标识符。
nullable,示意对象可能是空的,有\_\_nullable 和\_Nullable等价标识符。
null\_unspecified,不晓得对象是否为空,有\_\_null\_unspecified等价标识符。
OC标识符标注的对象类型和Swift类型对应关系如下:
除了以上标识符外,当初通过Xcode创立的头文件默认被 NS\_ASSUME\_NONNULL\_BEGIN 和 NS\_ASSUME\_NONNULL\_END 包住,即在这之间申明的对象默认标识符是 nonnull 的。
在Swift与OC混编场景,编译器会依据OC标识符将OC的对象类型转换成Swift类型,如果没有显式的标识,默认是null\_unspecified。例如:
@interface ExampleOCClass : NSObject// 没有指定标识符,且没有被NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END包裹,标识符默认为null_unspecified+ (ExampleOCClass *)getExampleObject; @end@implementation ExampleOCClass+ (ExampleOCClass *)getExampleObject { return nil; // OC代码间接返回nil}@end
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let _ = ExampleOCClass.getExampleObject().description // 报错:Thread 1: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value }}
在下面例子中,Swift代码调用OC接口获取一个对象,编译器隐式的将OC接口返回的对象转换为隐式解包类型来解决。因为隐式解包类型能够不显式解包间接应用,使用者往往会疏忽OC返回的是隐式解包类型,不通过判空而间接应用。但当代码执行时,因为OC接口返回了一个nil,导致Swift代码解包失败,产生运行时谬误。
在理论编码中,举荐显式指定OC对象为nonnull或者nullable,针对上述代码进行批改后如下:
@interface ExampleOCClass : NSObject/// 获取可空的对象+ (nullable ExampleOCClass *)getOptionalExampleObject;/// 获取不可空的对象+ (nonnull ExampleOCClass *)getNonOptionalExampleObject;@end@implementation ExampleOCClass+ (ExampleOCClass *)getOptionalExampleObject { return nil;}+ (ExampleOCClass *)getNonOptionalExampleObject { return [[ExampleOCClass alloc] init];}@end
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() // 标注nullable后,编译器调用接口时,会强制加上 ? let _ = ExampleOCClass.getOptionalExampleObject()?.description // 标注nonnull后,编译器将会把接口返回当做不可空来解决 let _ = ExampleOCClass.getNonOptionalExampleObject().description }}
在OC对象加上nonnull或者nullable标识符后,相当于给OC代码减少了相似Swift的『动态类型语言的个性』,使得编译器能够对代码进行可空类型检测,无效的升高了混编时解体的危险。但这种『动态个性』并不对OC齐全无效,例如以下代码,尽管申明返回类型是nonnull的,然而仍然能够返回nil:
@implementation ExampleOCClass+ (nonnull ExampleOCClass *)getNonOptionalExampleObject { return nil; // 接口申明不可空,但实际上返回一个空对象,能够通过编译,如果Swift当作非空对象应用,则会产生解体}@end
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() ExampleOCClass.getNonOptionalExampleObject().description }}
基于以上例子,仍然会产生运行时谬误。从安全性的角度上来说,仿佛Swift最好在应用所有OC的接口时都进行判空解决。但实际上这将导致Swift的代码充斥着大量冗余的判空代码,大大降低代码的可维护性,同时也违反了『裸露问题,而非暗藏问题』的编码准则,并不举荐这么做,正当的做法是在OC侧做好平安校验,OC对返回类型应做好测验,保障返回类型的正确性,以及返回值和标识符可能对应。
综合来看,OC侧标识符最好遵循如下应用准则:
1、不举荐应用NS\_ASSUME\_NONNULL\_BEGIN和NS\_ASSUME\_NONNULL\_END,因为默认修饰符是nonnull的,在理论开发中很容易疏忽返回的对象是否为空。返回空则会导致Swift运行时谬误。举荐所有波及混编的OC接口都须要显式应用相应的标识符润饰。
2、OC接口要审慎应用 nonnull 润饰 ,必须确保返回值不可能是空的状况下应用,任何不能确定不可空的接口都须要标注为nullable。
3、为防止Swift侧不必要的类型、判空等校验(违反Swift设计理念),在现实状态下需在OC侧进行类型的校验,保障返回对象和标注的标识符完全正确,这样Swift则能够齐全信赖OC返回的对象类型。
4、在Swift调用OC代码时,要关注OC返回的类型,尤其是返回隐式解包类型时,要做好判空解决。
5、在OC代码反对Swift调用前,提前对OC代码做好返回类型和标识符的查看,确保返回Swift的对象是平安的。
四、审慎应用强制类型转换
GEEK TALK
Swift 作为强类型语言,禁止所有默认类型转换,这要求编码者须要明确定义每一个变量的类型,在须要类型转换时必须显式的进行类型转换。Swift能够应用as和as?运算符进行类型转换。
as运算符用于强制类型转换,在类型兼容状况下,能够将一个类型转换为另一个类型,例如:
var d = 3.0 // 默认推断为 Double 类型var f: Float = 1.0 // 显式指定为 Float 类型d = f // 编译器将报错“Cannot assign value of type 'Float' to type 'Double'” d = f as Double // 须要将Float类型转换为Double类型,能力赋值给f
除了以上列举的根本类型外,Swift还兼容根底类型与对应的OC类型的转换,比方NSArray/Array、NSString/String、NSDictionary/Dictionary。
如果类型转换失败,将会导致运行时谬误。例如:
let string: Any = "string"let array = string as Array // 运行时谬误
这里string变量理论是一个String类型,尝试将String类型转换成Array类型,将导致运行时谬误。
另一种类型转换的形式是应用as?运算符,如果转换胜利,返回一个转换类型的可选类型,如果转换失败,返回nil。例如:
let string: Any = "string"let array = string as? Array // 转换失败,不会产生运行时谬误
这里因为无奈将String类型转换为Array类型,因而转换失败,array变量的值为nil,但不会产生运行时谬误。
综合来看,在进行类型转换时,须要留神以下几点:
1、类型转换只能在兼容的类型之间进行,例如Double和Float能够互相转换,但String和Array之间不能互相转换。
2、如果应用as进行强制类型转换,须要确保转换是平安的,否则将会导致运行时谬误。如果不能确保转换类型之间是兼容的,则应该应用as?运算符,例如将网络数据解析成模型数据时,无奈保障网络数据的类型,应该应用as?。
3、在应用as?运算符进行类型转换时,须要留神返回值可能为nil的状况。
---------- END ----------
举荐浏览【技术加油站】系列:
百度工程师挪动开发避坑指南——内存透露篇
百度程序员开发避坑指南(Go语言篇)
百度程序员开发避坑指南(3)
百度程序员开发避坑指南(挪动端篇)
百度程序员开发避坑指南(前端篇)