百度工程师移动开发避坑指南——Swift语言篇

412次阅读  |  发布于1年以前

作者 | 启明星小组
来自 百度Geek说 公众号

上一篇我们介绍了移动开发常见的内存泄漏问题,见《百度工程师移动开发避坑指南——内存泄漏篇》。本篇我们将介绍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的对象是安全的。

四、谨慎使用强制类型转换

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的情况。

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8