作者 | 启明星小组

上一篇咱们介绍了移动开发常见的内存走漏问题,见《百度工程师移动开发避坑攻略——内存走漏篇》。本篇咱们将介绍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类型对应联系如下:

百度工程师移动开发避坑指南——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)

百度程序员开发避坑攻略(移动端篇)

百度程序员开发避坑攻略(前端篇)

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