以上是一些常见的代码质量指标。我们的目标是如何更好的使用Swift
编写出符合代码质量指标要求的代码。
提示:本文不涉及设计模式/架构,更多关注如何通过合理使用
Swift
特性做部分代码段的重构。
一些不错的实践
1. 利用编译检查
减少使用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
进行通信。
- 1.
使用枚举关联值
代替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
}
复制代码
2. 内存安全
!
属性会在读取时隐式强解包
,当值不存在时产生运行时异常导致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类型一致,没有命名信息不清楚每个变量的含义
print("a \(a), b: \(b), c: \(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
自动修复部分不规范的代码。
常见的自动格式化修正
- 移除多余的
;
- 最多只保留一行换行
- 自动对齐
空格
- 限制每行的宽度
自动换行
作者:何乐乐 链接:juejin.cn/post/698476… 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
————————————————————————————————————————————