万字长文详解怎么运用Swift进步代码质量
前语
京喜APP
最早在2019年引入了Swift
,运用Swift
完成了第一个订单模块的开发。之后一年多咱们继续在团队/公司内部推行和遍及Swift
,现在Swift
现已支撑了70%+
以上的事务。经过运用Swift
进步了团队内同学的开发功率,一起也带来了质量的进步,现在来自Swift
的Crash的占比不到1%
。在这进程中不断的学习/实践,团队内的Code Review
,也对怎么运用Swift
来进步代码质量有更深的了解。
Swift特性
在评论怎么运用Swift
进步代码质量之前,咱们先来看看Swift
自身比较ObjC
或其他编程言语有什么优势。Swift
有三个重要的特性别离是赋有表现力
/安全性
/快速
,接下来咱们别离从这三个特性简略介绍一下:
赋有表现力
Swift
供给更多的编程范式
和特性
支撑,能够编写更少的代码,而且易于阅览和保护。
根底类型
- 元组、Enum相关类型
办法
-办法重载
protocol
- 不束缚只支撑class
、协议默许
完成、类
专属协议泛型
-protocol
相关类型、where
完成类型束缚、泛型扩展可选值
- 可选值声明、可选链、隐式可选值特点
- let、lazy、核算特点`、willset/didset、Property Wrappers函数式编程
- 调集filter/map/reduce
办法,供给更多标准库办法并发
- async/await、actor标准库结构
-Combine
呼应式结构、SwiftUI
声明式UI结构、Codable
JSON模型转化Result builder
- 描绘完成DSL
的才能动态性
- dynamicCallable、dynamicMemberLookup其他
- 扩展、subscript、操作符重写、嵌套类型、区间Swift Package Manager
- 根据Swift的包办理东西,能够直接用Xcode
进行办理更便利struct
- 初始化办法主动补齐类型揣度
- 经过编译器强壮的类型揣度
编写代码时能够削减许多类型声明
提示:类型揣度一起也会增加必定的编译耗时
,不过Swift
团队也在不断的改进编译速度。
安全性
代码安全
let特点
- 运用let
声明常量防止被批改。值类型
- 值类型能够防止在办法调用等参数传递
进程中状况被批改。拜访操控
- 经过public
和final
束缚模块外运用class
不能被承继
和重写
。强制反常处理
- 办法需求抛出反常时,需求声明为throw
办法。当调用或许会throw
反常的办法,需求强制捕获反常防止将反常露出到上层。形式匹配
- 经过形式匹配检测switch
中未处理的case。
类型安全
强制类型转化
- 制止隐式类型转化
防止转化中带来的反常问题。一起类型转化不会带来额定
的运行时耗费。。
提示:编写ObjC
代码时,咱们一般会在编码时增加类型查看防止运行时溃散导致Crash
。
KeyPath
-KeyPath
比较运用字符串
能够供给特点名和类型信息,能够运用编译器查看。泛型
- 供给泛型
和协议相关类型
,能够编写出类型安全的代码。比较Any
能够更多运用编译时查看发现类型问题。Enum相关类型
- 经过给特定枚举指定类型防止运用Any
。
内存安全
空安全
- 经过标识可选值防止空指针
带来的反常问题ARC
- 运用主动
内存办理防止手动
办理内存带来的各种内存问题强制初始化
- 变量运用前有必要初始化
内存独占拜访
- 经过编译器查看发现潜在的内存抵触问题
线程安全
值类型
- 更多运用值类型削减在多线程中遇到的数据竞赛
问题async/await
- 供给async
函数使咱们能够用结构化的办法编写并发操作。防止根据闭包
的异步办法带来的内存循环引证
和无法抛出反常的问题Actor
- 供给Actor
模型防止多线程开发中进行数据同享时发生的数据竞赛问题,一起防止在运用锁时带来的死锁等问题
快速
值类型
- 比较class
不需求额定的堆内存
分配/开释和更少的内存耗费办法静态派发
- 办法调用支撑静态
调用比较原有ObjC音讯转发
调用功用更好编译器优化
- Swift的静态性
能够使编译器做更多优化。例如Tree Shaking
相关优化移除未运用的类型/办法等削减二进制文件巨细。运用静态派发
/办法内联优化
/泛型特化
/写时仿制
等优化进步运行时功用
提示:ObjC
音讯派发会导致编译器无法进行移除无用办法/类的优化,编译器并不知道是否或许被用到。
ARC优化
- 尽管和ObjC
相同都是运用ARC
,Swift
经过编译器优化,能够进行更快的内存收回和更少的内存引证计数办理
提示: 比较ObjC
,Swift内部不需求运用autorelease
进行办理。
代码质量指标
以上是一些常见的代码质量指标。咱们的方针是怎么更好的运用Swift
编写出契合代码质量指标要求的代码。
提示:本文不触及规划形式/架构,更多重视怎么经过合理运用Swift
特性做部分代码段的重构。
一些不错的实践
运用编译查看
削减运用Any/AnyObject
由于Any/AnyObject
短少明晰的类型信息,编译器无法进行类型查看,会带来一些问题:
- 编译器无法查看类型是否正确确保类型安全
- 代码中许多的
as?
转化 - 类型的缺失导致编译器无法做一些潜在的
编译优化
运用as?
带来的问题
当运用Any/AnyObject
时会频频运用as?
进行类型转化。这如同没什么问题由于运用as?
并不会导致程序Crash
。不过代码过错至少应该分为两类,一类是程序自身的过错一般会引发Crash,别的一种是事务逻辑过错。运用as?
仅仅防止了程序过错Crash
,可是并不能防止事务逻辑过错。
func do(data: Any?) {
guard let string = data as? String else {
return
}
//
}
do(1)
do("")
以上面的比如为例,咱们进行了as?
转化,当data
为String
时才会进行处理。可是当do
办法内String
类型发生了改动函数,运用方并不知道已改动没有做相应的适配,这时分就会形成事务逻辑的过错。
提示:这类过错一般更难发现,这也是咱们在一次实在bug
场景遇到的。
运用自界说类型
代替Dictionary
代码中许多Dictionary
数据结构会下降代码可保护性,一起带来潜在的bug
:
key
需求字符串硬编码,编译时无法查看value
没有类型束缚。批改
时类型无法束缚,读取时需求重复类型转化和解包操作- 无法运用
空安全
特性,指定某个特点有必要有值
提示:自界说类型
还有个优点,例如JSON
转自界说类型
时会进行类型/nil/特点名
查看,能够防止将过错数据丢到下一层。
不引荐
let dic: [String: Any]
let num = dic["value"] as? Int
dic["name"] = "name"
引荐
struct Data {
let num: Int
var name: String?
}
let num = data.num
data.name = "name"
适宜运用Dictionary
的场景
数据不运用
- 数据并不读取
仅仅用来传递。解耦
- 1.组件间通讯
解耦运用HashMap
传递参数进行通讯。2.跨技能栈鸿沟的场景,混合栈间通讯/前后端通讯
运用HashMap
/JSON
进行通讯。
运用枚举相关值
代替Any
例如运用枚举改造NSAttributedString
API,原有APIvalue
为Any
类型无法束缚特定的类型。
优化前
let string = NSMutableAttributedString()
string.addAttribute(.foregroundColor, value: UIColor.red, range: range)
改造后
enum NSAttributedStringKey {
case foregroundColor(UIColor)
}
let string = NSMutableAttributedString()
string.addAttribute(.foregroundColor(UIColor.red), range: range) // 不传递Color会报错
运用泛型
/协议相关类型
代替Any
运用泛型
或协议相关类型
代替Any
,经过泛型类型束缚
来使编译器进行更多的类型查看。
运用枚举
/常量
代替硬编码
代码中存在重复的硬编码
字符串/数字,在批改时或许会由于不同步引发bug
。尽或许削减硬编码
字符串/数字,运用枚举
或常量
代替。
运用KeyPath
代替字符串
硬编码
KeyPath
包括特点名和类型信息,能够防止硬编码
字符串,一起当特点名或类型改动时编译器会进行查看。
不引荐
class SomeClass: NSObject {
@objc dynamic var someProperty: Int
init(someProperty: Int) {
self.someProperty = someProperty
}
}
let object = SomeClass(someProperty: 10)
object.observeValue(forKeyPath: "", of: nil, change: nil, context: nil)
引荐
let object = SomeClass(someProperty: 10)
object.observe(.someProperty) { object, change in
}
内存安全
削减运用!
特点
!
特点会在读取时隐式强解包
,当值不存在时发生运行时反常导致Crash。
class ViewController: UIViewController {
@IBOutlet private var label: UILabel! // @IBOutlet需求运用!
}
削减运用!
进行强解包
运用!
强解包会在值不存在时发生运行时反常导致Crash。
var num: Int?
let num2 = num! // 过错
提示:主张只在小范围的部分代码段运用!
强解包。
防止运用try!
进行过错处理
运用try!
会在办法抛出反常时发生运行时反常导致Crash。
try! method()
运用weak
/unowned
防止循环引证
resource.request().onComplete { [weak self] response in
guard let self = self else {
return
}
let model = self.updateModel(response)
self.updateUI(model)
}
resource.request().onComplete { [unowned self] response in
let model = self.updateModel(response)
self.updateUI(model)
}
削减运用unowned
unowned
在值不存在时会发生运行时反常导致Crash,只要在确认self
必定会存在时才运用unowned
。
class Class {
@objc unowned var object: Object
@objc weak var object: Object?
}
unowned
/weak
差异:
weak
- 有必要设置为可选值,会进行弱引证处理功用更差。会主动设置为nil
unowned
- 能够不设置为可选值,不会进行弱引证处理功用更好。可是不会主动设置为nil
, 假如self
已开释会触发过错.
过错处理办法
可选值
- 调用方并不重视内部或许会发生过错,当发生过错时回来nil
try/catch
- 明晰提示调用方需求处理反常,需求完成Error
协议界说明晰的过错类型assert
- 断语。只能在Debug
形式下收效precondition
- 和assert
相似,能够再Debug
/Release
形式下收效fatalError
- 发生运行时溃散会导致Crash,应防止运用Result
- 一般用于闭包
异步回调回来值
削减运用可选值
可选值
的价值在于经过明晰标识值或许会为nil
而且编译器强制对值进行nil
判别。可是不应该随意的界说可选值,可选值不能用let
界说,而且运用时有必要进行解包
操作相对比较繁琐。在代码规划时应考虑这个值是否有或许为nil
,只在适宜的场景运用可选值。
运用init
注入代替可选值
特点
不引荐
class Object {
var num: Int?
}
let object = Object()
object.num = 1
引荐
class Object {
let num: Int
init(num: Int) {
self.num = num
}
}
let object = Object(num: 1)
防止随意给予可选值默许值
在运用可选值时,一般咱们需求在可选值为nil
时进行反常处理。有时分咱们会经过给予可选值默许值
的办法来处理。可是这儿应考虑在什么场景下能够给予默许值。在不能给予默许值的场景应当及时运用return
或抛出反常
,防止过错的值被传递到更多的事务流程。
不引荐
func confirmOrder(id: String) {}
// 给予过错的值会导致过错的值被传递到更多的事务流程
confirmOrder(id: orderId ?? "")
引荐
func confirmOrder(id: String) {}
guard let orderId = orderId else {
// 反常处理
return
}
confirmOrder(id: orderId)
提示:一般强事务相关的值不能给予默许值:例如产品/订单id
或是价格
。在能够运用兜底逻辑的场景运用默许值,例如默许文字/文字色彩
。
运用枚举优化可选值
Object
结构一起只会有一个值存在:
优化前
class Object {
var name: Int?
var num: Int?
}
优化后
下降内存占用
-枚举相关类型
的巨细取决于最大的相关类型巨细逻辑更明晰
- 运用enum
比较许多运用if/else
逻辑更明晰
enum CustomType {
case name(String)
case num(Int)
}
削减var
特点
运用核算特点
运用核算特点
能够削减多个变量同步带来的潜在bug。
不引荐
class model {
var data: Object?
var loaded: Bool
}
model.data = Object()
loaded = false
引荐
class model {
var data: Object?
var loaded: Bool {
return data != nil
}
}
model.data = Object()
提示:核算特点由于每次都会重复核算,所以核算进程需求轻量防止带来功用问题。
操控流
运用filter/reduce/map
代替for
循环
运用filter/reduce/map
能够带来许多优点,包括更少的部分变量,削减模板代码,代码愈加明晰,可读性更高。
不引荐
let nums = [1, 2, 3]
var result = []
for num in nums {
if num < 3 {
result.append(String(num))
}
}
// result = ["1", "2"]
引荐
let nums = [1, 2, 3]
let result = nums.filter { $0 < 3 }.map { String($0) }
// result = ["1", "2"]
运用guard
进行提早回来
引荐
guard !a else {
return
}
guard !b else {
return
}
// do
不引荐
if a {
if b {
// do
}
}
运用三元运算符?:
引荐
let b = true
let a = b ? 1 : 2
let c: Int?
let b = c ?? 1
不引荐
var a: Int?
if b {
a = 1
} else {
a = 2
}
运用for where
优化循环
for
循环增加where
句子,只要当where
条件满意时才会进入循环
不引荐
for item in collection {
if item.hasProperty {
// ...
}
}
引荐
for item in collection where item.hasProperty {
// item.hasProperty == true,才会进入循环
}
运用defer
defer
能够确保在函数退出前必定会履行。能够运用defer
中完成退出时必定会履行的操作例如资源开释
等防止遗失。
func method() {
lock.lock()
defer {
lock.unlock()
// 会在method效果域完毕的时分调用
}
// do
}
字符串
运用"""
在界说杂乱
字符串时,运用多行字符串字面量
能够坚持原有字符串的换行符号/引号等特别字符,不需求运用``进行转义。
let quotation = """
The White Rabbit put on his spectacles. "Where shall I begin,
please your Majesty?" he asked.
"Begin at the beginning," the King said gravely, "and go on
till you come to the end; then stop."
"""
提示:上面字符串中的""
和换行能够主动保存。
运用字符串插值
运用字符串插值能够进步代码可读性。
不引荐
let multiplier = 3
let message = String(multiplier) + "times 2.5 is" + String((Double(multiplier) * 2.5))
引荐
let multiplier = 3
let message = "(multiplier) times 2.5 is (Double(multiplier) * 2.5)"
调集
运用标准库供给的高阶函数
不引荐
var nums = []
nums.count == 0
nums[0]
引荐
var nums = []
nums.isEmpty
nums.first
拜访操控
Swift
中默许拜访操控等级为internal
。编码中应当尽或许减小特点
/办法
/类型
的拜访操控等级躲藏内部完成。
提示:一起也有利于编译器进行优化。
运用private
/fileprivate
润饰私有特点
和办法
private let num = 1
class MyClass {
private var num: Int
}
运用private(set)
润饰外部只读/内部可读写特点
class MyClass {
private(set) var num = 1
}
let num = MyClass().num
MyClass().num = 2 // 会编译报错
函数
运用参数默许值
运用参数默许值
,能够使调用方传递更少
的参数。
不引荐
func test(a: Int, b: String?, c: Int?) {
}
test(1, nil, nil)
引荐
func test(a: Int, b: String? = nil, c: Int? = nil) {
}
test(1)
提示:比较ObjC
,参数默许值
也能够让咱们界说更少的办法。
束缚参数数量
当办法参数过多时考虑运用自界说类型
代替。
不引荐
func f(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {
}
引荐
struct Params {
let a, b, c, d, e, f: Int
}
func f(params: Params) {
}
运用@discardableResult
某些办法运用方并不必定会处理回来值,能够考虑增加@discardableResult
标识提示Xcode
答应不处理回来值不进行warning
提示。
// 上报办法运用方不关心是否成功
func report(id: String) -> Bool {}
@discardableResult func report2(id: String) -> Bool {}
report("1") // 编译器会正告
report2("1") // 不处理回来值编译器不会正告
元组
防止过长的元组
元组尽管具有类型信息,可是并不包括变量名
信息,运用方并不明晰知道变量的意义。所以当元组数量过多时考虑运用自界说类型
代替。
func test() -> (Int, Int, Int) {
}
let (a, b, c) = test()
// a,b,c类型共同,没有命名信息不清楚每个变量的意义
体系库
KVO
/Notification
运用 block
API
block
API的优势:
KVO
能够支撑KeyPath
- 不需求主动移除监听,
observer
开释时主动移除监听
不引荐
class Object: NSObject {
init() {
super.init()
addObserver(self, forKeyPath: "value", options: .new, context: nil)
NotificationCenter.default.addObserver(self, selector: #selector(test), name: NSNotification.Name(rawValue: ""), object: nil)
}
override class func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
}
@objc private func test() {
}
deinit {
removeObserver(self, forKeyPath: "value")
NotificationCenter.default.removeObserver(self)
}
}
引荐
class Object: NSObject {
private var observer: AnyObserver?
private var kvoObserver: NSKeyValueObservation?
init() {
super.init()
observer = NotificationCenter.default.addObserver(forName: NSNotification.Name(rawValue: ""), object: nil, queue: nil) { (_) in
}
kvoObserver = foo.observe(.value, options: [.new]) { (foo, change) in
}
}
}
Protocol
运用protocol
代替承继
Swift
中针对protocol
供给了许多新特性,例如默许完成
,相关类型
,支撑值类型。在代码规划时能够优先考虑运用protocol
来防止臃肿的父类一起更多运用值类型。
提示:一些无法用protocol
代替承继
的场景:1.需求承继NSObject子类。2.需求调用super
办法。3.完成抽象类
的才能。
Extension
运用extension
安排代码
运用extension
将私有办法
/父类办法
/协议办法
等不同功用代码进行别离愈加明晰/易保护。
class MyViewController: UIViewController {
// class stuff here
}
// MARK: - Private
extension: MyViewController {
private func method() {}
}
// MARK: - UITableViewDataSource
extension MyViewController: UITableViewDataSource {
// table view data source methods
}
// MARK: - UIScrollViewDelegate
extension MyViewController: UIScrollViewDelegate {
// scroll view delegate methods
}
代码风格
杰出的代码风格能够进步代码的可读性
,一致的代码风格能够下降团队内彼此了解本钱
。关于Swift
的代码格式化
主张运用主动格式化东西完成,将主动格式化增加到代码提交流程,经过界说Lint规矩
一致团队内代码风格。考虑运用SwiftFormat
和SwiftLint
。
提示:SwiftFormat
首要重视代码款式的格式化,SwiftLint
能够运用autocorrect
主动批改部分不标准的代码。
常见的主动格式化批改
- 移除剩余的
;
- 最多只保存一行换行
- 主动对齐
空格
- 束缚每行的宽度
主动换行
功用优化
功用优化上首要重视进步运行时功用
和下降二进制体积
。需求考虑怎么更好的运用Swift
特性,一起供给更多信息给编译器
进行优化。