作者 | 启明星小组
上一篇咱们介绍了挪动开发常见的内存透露问题,见《百度工程师挪动开发避坑指南——内存透露篇》。本篇咱们将介绍 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)
百度程序员开发避坑指南(挪动端篇)
百度程序员开发避坑指南(前端篇)