阅读 792

Swift 代码质量指标

image.png

以上是一些常见的代码质量指标。我们的目标是如何更好的使用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?转换,当dataString时才会进行处理。但是当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

例如使用枚举改造NSAttributedStringAPI,原有APIvalueAny类型无法限制特定的类型。

优化前
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规则统一团队内代码风格。考虑使用SwiftFormatSwiftLint

提示:SwiftFormat主要关注代码样式的格式化,SwiftLint可以使用autocorrect自动修复部分不规范的代码。

常见的自动格式化修正
  • 移除多余的;
  • 最多只保留一行换行
  • 自动对齐空格
  • 限制每行的宽度自动换行

作者:何乐乐 链接:juejin.cn/post/698476… 来源:稀土掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

[Swift 编程官网规范]

————————————————————————————————————————————

文章分类
iOS