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


    在这里插入图片描述
    作者 | 启明星小组

    上一篇我们介绍了移动开发常见的内存泄漏问题,见《百度工程师移动开发避坑指南——内存泄漏篇》。本篇我们将介绍Swift语言部分常见问题。

    对于Swift开发者,Swift较于OC一个很大的不同就是引入了可选类型(Optional),刚接触Swift的开发者很容易在相关代码上踩坑。

    本期我们带来与Swift可选类型相关的几个避坑指南:可选类型要判空;避免使用隐式解包可选类型;合理使用Objective-C标识符;谨慎使用强制类型转换。希望能对Swift开发者有所帮助。

    一、可选类型(Optional)要判空

    在Objective-C中,可以使用nil来表示对象为空,但是使用一个为nil的对象通常是不安全的,如果使用不慎会出现崩溃或者其它异常问题。在Swift中,开发者可以使用可选类型表示变量有值或者没有值,可以更加清晰的表达类型是否可以安全的使用。如果一个变量可能为空,那么在声明时可以使用?来表示,使用前需要进行解包。例如:

    var optionalString: String?
    
    • 1

    在使用可选类型对象时,需要进行解包操作,有两种解包方式:强制解包与可选绑定。

    强制解包使用 ! 修饰一个可选对象 ,相当于告诉编译器『我知道这是一个可选类型,但在这里我可以保证他不为空,编译时请忽略此处的可空校验』,例如:

    let unwrappedString: String = optionalString!  // 运行时报错:Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
    
    • 1

    这里使用 ! 进行了强制解包,如果optionalString为nil,将会产生运行时错误,发生崩溃。**因此,在使用 ! 进行强制解包时,必须保证变量不为nil,要对变量进行判空处理,**如下:

    if optionalString != nil {
        let unwrappedString = optionalString!
    }
    
    • 1
    • 2
    • 3

    相较于强制解包的不安全性,一般而言推荐另一种解包方式,即可选绑定。例如:

    if let optionalString = optionalString {
        // 这里optionalString不为nil,是已经解包后的类型,可以直接使用
    }
    
    • 1
    • 2
    • 3

    综上,在对可选类型进行解包时应尽量避免使用强制解包,采用可选绑定替代。如果一定要使用强制解包,那么必须在逻辑上完全保证类型不为空,并且做好注释工作,以增加后续代码的可维护性。

    二、避免使用隐式解包可选类型(Implicitly Unwrapped Optionals)

    由于可选类型每次使用之前都需要进行显式解包操作,有时变量在第一次赋值之后,就会一直有值,如果每次使用都显式解包,显得繁琐,Swift引入了隐式解包可选类型,隐式解包可选类型可以使用 ! 来表示,并且使用时不需要显式解包,可以直接使用,例如:

    var implicitlyUnwrappedOptionalString: String! = "implicitlyUnwrappedOptionalString"
    var implicitlyString: String = implicitlyUnwrappedOptionalString
    
    • 1
    • 2

    上述例子的隐式解包,在编译和运行过程中都不会发生问题,但如果在两行代码中间插入一行 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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    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
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    在上面例子中,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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    class ViewController: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            // 标注nullable后,编译器调用接口时,会强制加上 ?
            let _ = ExampleOCClass.getOptionalExampleObject()?.description 
            // 标注nonnull后,编译器将会把接口返回当做不可空来处理
            let _ = ExampleOCClass.getNonOptionalExampleObject().description 
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在OC对象加上nonnull或者nullable标识符后,相当于给OC代码增加了类似Swift的『静态类型语言的特性』,使得编译器可以对代码进行可空类型检测,有效的降低了混编时崩溃的风险。但这种『静态特性』并不对OC完全有效,例如以下代码,虽然声明返回类型是nonnull的,但是依然可以返回nil:

    @implementation ExampleOCClass
    + (nonnull ExampleOCClass *)getNonOptionalExampleObject {
        return nil; // 接口声明不可空,但实际上返回一个空对象,可以通过编译,如果Swift当作非空对象使用,则会发生崩溃
    }
    @end
    
    • 1
    • 2
    • 3
    • 4
    • 5
    class ViewController: UIViewController {
        override func viewDidLoad() {
            super.viewDidLoad()
            ExampleOCClass.getNonOptionalExampleObject().description
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    基于以上例子,依然会产生运行时错误。从安全性的角度上来说,似乎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
    
    • 1
    • 2
    • 3
    • 4

    除了以上列举的基本类型外,Swift还兼容基础类型与对应的OC类型的转换,比如NSArray/Array、NSString/String、NSDictionary/Dictionary。

    如果类型转换失败,将会导致运行时错误。例如:

    let string: Any = "string"
    let array = string as Array // 运行时错误
    
    • 1
    • 2

    这里string变量实际是一个String类型,尝试将String类型转换成Array类型,将导致运行时错误。

    另一种类型转换的方式是使用as?运算符,如果转换成功,返回一个转换类型的可选类型,如果转换失败,返回nil。例如:

    let string: Any = "string"
    let array = string as? Array // 转换失败,不会产生运行时错误
    
    • 1
    • 2

    这里由于无法将String类型转换为Array类型,因此转换失败,array变量的值为nil,但不会产生运行时错误。

    综合来看,在进行类型转换时,需要注意以下几点:

    1、类型转换只能在兼容的类型之间进行,例如Double和Float可以相互转换,但String和Array之间不能相互转换。

    2、如果使用as进行强制类型转换,需要确保转换是安全的,否则将会导致运行时错误。如果不能确保转换类型之间是兼容的,则应该使用as?运算符,例如将网络数据解析成模型数据时,无法保证网络数据的类型,应该使用as?。

    3、在使用as?运算符进行类型转换时,需要注意返回值可能为nil的情况。

    ---------- END ----------

    推荐阅读【技术加油站】系列:

    百度工程师移动开发避坑指南——内存泄漏篇

    百度程序员开发避坑指南(Go语言篇)

    百度程序员开发避坑指南(3)

    百度程序员开发避坑指南(移动端篇)

    百度程序员开发避坑指南(前端篇)

    图片

  • 相关阅读:
    HTTP 协议内容的介绍与应用
    OceanBase开发者大会2023届视频及PPT汇总
    Vue3 中如何加载动态菜单?
    git branch 分支
    9、Neural Sparse Voxel Fields
    记一次spark数据倾斜实践
    查看linux下dns信息并修改
    vue 项目代码混淆配置(自定义插件适用)带配置项注释
    Ubuntu2204 搭建TFTP 服务
    自动化测试中对数据恢复的思考与实际业务改造实践
  • 原文地址:https://blog.csdn.net/lihui49/article/details/130842143