当前位置:首页 > 后端开发 > 正文内容

万字长文详解怎么运用Swift进步代码质量

邻居的猫1个月前 (12-09)后端开发473

前语

京喜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结构、CodableJSON模型转化
  • Result builder - 描绘完成DSL的才能
  • 动态性 - dynamicCallable、dynamicMemberLookup
  • 其他 - 扩展、subscript、操作符重写、嵌套类型、区间
  • Swift Package Manager - 根据Swift的包办理东西,能够直接用Xcode进行办理更便利
  • struct - 初始化办法主动补齐
  • 类型揣度 - 经过编译器强壮的类型揣度编写代码时能够削减许多类型声明

提示:类型揣度一起也会增加必定的编译耗时,不过Swift团队也在不断的改进编译速度。

安全性

代码安全

  • let特点 - 运用let声明常量防止被批改。
  • 值类型 - 值类型能够防止在办法调用等参数传递进程中状况被批改。
  • 拜访操控 - 经过publicfinal束缚模块外运用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相同都是运用ARCSwift经过编译器优化,能够进行更快的内存收回和更少的内存引证计数办理

提示: 比较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?转化,当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
}


内存安全

削减运用!特点

!特点会在读取时隐式强解包,当值不存在时发生运行时反常导致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规矩一致团队内代码风格。考虑运用SwiftFormatSwiftLint

提示:SwiftFormat首要重视代码款式的格式化,SwiftLint能够运用autocorrect主动批改部分不标准的代码。

常见的主动格式化批改

  • 移除剩余的;
  • 最多只保存一行换行
  • 主动对齐空格
  • 束缚每行的宽度主动换行

功用优化

功用优化上首要重视进步运行时功用和下降二进制体积。需求考虑怎么更好的运用Swift特性,一起供给更多信息给编译器进行优化。

运用

扫描二维码推送至手机访问。

版权声明:本文由51Blog发布,如需转载请注明出处。

本文链接:https://www.51blog.vip/?id=215

分享给朋友:

“万字长文详解怎么运用Swift进步代码质量” 的相关文章

python程序设计基础,Python程序设计基础入门指南

学习Python程序设计基础,可以按照以下步骤进行:1. 了解Python及其特点: Python是一种高级编程语言,以其简洁、易读和易学而闻名。 它是一种解释型语言,意味着不需要编译。 Python广泛应用于数据科学、Web开发、自动化、人工智能等领域。2. 安装Python:...

r语言培训,开启数据科学之旅

1. 哔哩哔哩课程 小白速成与实践 全30课:这套课程包括30条视频,涵盖了R语言的基础知识,如R语言是什么、R的优势、资源、安装、获取帮助、工作空间管理、R包的使用等。 课程标准:提供从基础到高级的R语言教程,包括数据挖掘与R语言,以及各种统计分析方法的应用。2. 华大基因培训...

go ping,Go语言简介

在Go语言中,实现ping功能有多种方法,包括使用第三方库和手动实现。以下是几种常见的方法: 使用第三方库1. goping库: 简介:goping是一个简洁但功能强大的ICMP回显(ping)库,可以发送和接收ICMP数据包。 安装:使用`go get u github.com/gop...

java工具,提升效率的利器

java工具,提升效率的利器

1. 集成开发环境(IDEs): IntelliJ IDEA:由 JetBrains 开发,功能强大,适合大型项目。 Eclipse:开源的 IDE,广泛用于 Java 开发。 NetBeans:另一个开源的 IDE,适合初学者。 Visual Studio Code:虽然不...

c语言的三种基本结构,构建高效程序的基石

C语言的三种基本结构是顺序结构、选择结构和循环结构。1. 顺序结构:顺序结构是最基本的结构,它按照程序代码的先后顺序执行。在顺序结构中,程序从第一条语句开始执行,然后依次执行后续的语句,直到程序结束。2. 选择结构:选择结构用于根据不同的条件执行不同的语句。在C语言中,选择结构主要有两种形式:if语...

rust木门怎么拆,Rust游戏中的木门拆除方法详解

rust木门怎么拆,Rust游戏中的木门拆除方法详解

拆装木门是一项需要谨慎操作的任务,尤其是对于初学者来说。下面是一些基本的步骤,可以帮助你安全地拆下Rust木门:1. 准备工具:在开始之前,确保你拥有必要的工具,如螺丝刀、锤子、凿子、钳子等。2. 断电:如果门附近有电源插座或开关,请先关闭电源,以避免触电风险。3. 拆卸门把手和锁:首先,卸下门把手...