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

news2024/11/20 10:44:37

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

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

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

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

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

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

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

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

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

图片

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/562449.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Install Redis Cluster(1master-2slave) on Kubernetes

目录 Node & Software & Docker Images Lists Prerequisites Architecture Setting up your Redis cluster Creating Namespace Creating StorageClass Creating Persistent volumes Creating ConfigMap Creating StatefulSet Creating Headless Service …

中创|警惕AI骗局,10分钟被骗430万,AI诈骗正在全国爆发!

眼见为实?耳听为真?当心AI诈骗! 只需要提供一张带脸的照片,就可以把自己置换成视频、电视剧中的男(女)主角,拟真度非常高,毫无违和感,这是最近爆火的AI换脸。 然而随着人…

浏览器数据存储方式

浏览器数据存储方式 常用的前端数据存储方法笼统来说有 3 种: local/session storagecookiesindexeddb 3 种方法各有各的优点和使用范围。 local/session storage local/session storage 保存的格式都为键值对,并且用法都是差不多,如下&…

如何选择高品质SPD浪涌保护器

了解了SPD的原理和技术参数和选型方法,但是面对市场上形形色色的SPD品牌,相差无几的参数,该如何去筛选高品质的SPD呢? 作为一个SPD开发人员,谈一下我的看法。前面提到,选择SPD时,有几个重要的参…

探索 Python Web 后端技术的发展之路

导语 Python 在 Web 后端开发领域中有着广泛的应用,它简洁的语法和强大的功能使得开发者们青睐有加。本文将更深入地探讨 Python Web 后端技术的发展趋势和路线,以及相关技术如何影响了 Web 开发的未来。 一、Python Web 框架的演变 Flask&#xff1a…

软件设计师 软件工程

** 判定覆盖 设置判定用例来保障真和假的结果都可以取到** 满足条件覆盖问题问需要多少个测试 ** 其实有技巧的(就看最后面的 分支)** **沟通路径:(n-1)n再/2 和主程序沟通那就是n-1条 ** ******************* 做题技…

HTTPS行为大赏(三分钟了解加密过程)

文章目录 前言1.没有加密的时候2.对称密钥加密传输3.非对称加密4.引入数字证书(对称加密非对称加密) 前言 既然要对HTTPS进行解读,我们首先了解,HTTPS是什么?HTTPS就相当于HTTPSSL/TLS这样的组合,HTTP&…

软考 软件设计师计算机网络笔记

网络设备 物理层的互联设备有中继器和集线器,集线器是一种特殊的多路多端口中继器 数据链路层的互连设备有网桥,交换机,交换机是一个多端口的网桥 网络层互连设备有路由器 协议簇 所有带T的除了TFTP其他都是TCP,所有不带T的除…

BFT 最前线 | ChatGPT登顶App Store;国产中文大语言模型「天河天元」发布;华为招募天才少年;阿里分拆上市

原创 | 文 BFT机器人 AI视界 TECHNOLOGY NEWS 01 ChatGPT上架App Store登顶榜首 OpenAI:很快也将出现在安卓上 近日,ChatGPT正式发布App版本,上架APP Store,支持iPhone和iPad设备。OpenAI表示,ChatGPT iOS APP可免费…

两阶段鲁棒优化及列与约束生成算法(CCG)的基本原理(超详细讲解,附matlab代码)

本文的主要参考文献: Zeng B , Zhao L . Solving Two-stage Robust Optimization Problems by A Constraint-and-Column Generation Method[J]. Operations Research Letters, 2013, 41(5):457-461. 1.两阶段鲁棒优化问题的引入 鲁棒优化是应对数据不确定性的一种优…

探索【Stable-Diffusion WEBUI】的图片超分辨插件:StableSR

文章目录 (零)前言(一)图片放大(二)图片超分辨率放大脚本插件(StableSR)(2.1)下载组件(2.2)使用(2.3)实例对比…

bat脚本语法与实战

一、什么是bat脚本 bat脚本就是将一系列DOS命令按照一定顺序排列而形成的集合,运行在windows命令行环境上。通过本文的学习,基本可以实现一些简单的脚本。 二、为什么学习bat脚本? 使用bat可以提高办公效率,可以直接使用Notepad编…

JavaEE(系列12) -- 常见锁策略

目录 1. 乐观锁和悲观锁 2. 轻量级锁与重量级锁 3. 自旋锁和挂起等待锁 4. 互斥锁和读写锁 5. 可重入锁与不可重入锁 6. 死锁 6.1 死锁的必要条件 6.2 如何避免死锁 7. 公平锁和非公平锁 8. Synchronized原理及加锁过程 8.1 Synchronized 小结 8.2 加锁工作过程 8.2.1 偏向锁…

MySQL保证主备一致,如何解决循环复制?

备库只读,是如何和主库同步数据的? 你可能会问,我把备库设置成只读了,还怎么跟主库保持同步更新呢? 这个问题,你不用担心。因为 readonly 设置对超级 (super) 权限用户是无效的,而用于同步更新…

用Typescript 的方式封装Vue3的表单绑定,支持防抖等功能。

Vue3 的父子组件传值、绑定表单数据、UI库的二次封装、防抖等,想来大家都很熟悉了,本篇介绍一种使用 Typescript 的方式进行统一的封装的方法。 基础使用方法 Vue3对于表单的绑定提供了一种简单的方式:v-model。对于使用者来说非常方便&…

【011】C++选择控制语句 if 和 switch 详解

C控制语句之if和switch语句 引言一、选择控制语句if1.1、if 语句的形式1.2、if...else...语句的形式1.3、if...else if... else...语句 二、选择控制语句switch2.1、switch语句形式 三、switch和if...else if...else...比较四、注意事项总结 引言 💡 作者简介&#…

企业工程行业管理系统源码-专业的工程管理软件-提供一站式服务

Java版工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离 功能清单如下: 首页 工作台:待办工作、消息通知、预警信息,点击可进入相应的列表 项目进度图表:选择(总体或单个)项目显示1…

Doo Prime 德璞资本:期货开户条件全解析!让你不再困惑!

期货市场是金融市场中一个非常重要的部分,对于许多投资者来说,期货市场是一个非常有吸引力的投资选择。然而,要进行期货交易,必须首先开设期货账户,这就需要满足一些期货开户条件,因此本文将介绍期货开户条…

认识SpringCloud(一) 注册中心Eureka

Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务治理。在传统的rpc远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,管理服务于服务之间依赖关系,可以实现服…

(原创)getX+Dio实现Flutter悬浮置顶的页面效果

前言 Flutter的开发相对已经比较成熟了,现在市面上不少商业应用也在使用这个技术 老实说,Flutter去实现一些基础的ui界面,效率还是很高的 当然前提是你对它要有一定的了解。 今天就演示一下,如何去实现一个基础悬浮置顶的页面效果…