Swift-链式开发思想
一. 什么是链式?
可以连续不断地进⾏方法调⽤用的一种语法形式。
二. 探究链式的使用与实现本质
示例1:打豆豆
有位科学家到了南极,碰到一群企鹅。他问其中一个:“你每天都干什么呀?”那企鹅说:“吃饭睡觉打豆豆。”
他又问另一个:“你每天都干什么呀?”那企鹅也源说:“吃饭睡觉打豆豆。”
他问了许多许多的企鹅,都说:“吃饭睡觉打豆豆。”
后来他碰到了一只小企鹅,很可爱的样子,就问它:“小朋友,你每天都干什么呀?”小企鹅说:“吃饭睡觉。”科学家一愣,随即问到:“你怎么不打豆豆呀?”小企鹅委屈的说:“因为我就是豆豆。”
思考一下如何用代码表述这些企鹅每天都做了什么❓
普通的实现方式
// 其他企鹅的一天
let otherPenguin = Penguin()
otherPenguin.eat()
otherPenguin.sleep()
otherPenguin.strike(penguin: "豆豆")
print(otherPenguin.description)
// ->吃饭->睡觉->打豆豆
使用链式实现
let longDay = Penguin.start { (make) in
make.eat().sleep().strike("豆豆")
}
print(penguin.description)
// ->吃饭->睡觉->打豆豆
// 企鹅类
class Penguin {
fileprivate var description: String = ""
static func start(block: (Penguin) -> Void) -> String {
let penguin = Penguin()
block(penguin)
return penguin.description
}
}
extension Penguin {
@discardableResult
func eat() -> Self {
description += "->吃饭"
return self
}
@discardableResult
func sleep() -> Penguin {
description += "->睡觉"
return self
}
@discardableResult
func strike(_ name: String) -> Penguin {
description += "->打\(name)"
return self
}
}
通过这个示例可以发现链式表达优点:精简代码,提升代码的阅读性。
示例2:实现简单的计算器功能
let result = Calculator.begin { (maker) in
maker.add(n: 2).subtract(n: 2).add(n: 3).divide(n: 0)
}
print(result)
public class Calculator {
public static func begin(caculateBlock:(CaculateMaker) -> ()) -> Double {
let caculator = CaculateMaker()
caculateBlock(caculator)
return caculator.result
}
}
public class CaculateMaker {
public var result: Double = 0
/// 加法
@discardableResult
public func add(n: Double) -> CaculateMaker {
result += n
return self
}
/// 减法
@discardableResult
public func subtract(n: Double) -> CaculateMaker {
result -= n
return self
}
/// 乘法
@discardableResult
public func multiply(n: Double) -> CaculateMaker {
result *= n
return self
}
/// 除法
@discardableResult
public func divide(n: Double) -> CaculateMaker? {
if n == 0 {
result = 0
} else {
result /= n
}
return self
}
}
示例3: Swift的高级函数的使用
实现对数组去nil处理,并对数组排序的需求
let countArray = [1, 2, 4, 5, nil, 3]
let resultArr = countArray.compactMap { $0 }.sorted(by: <)
print(resultArr)
// [1, 2, 3, 4, 5]
查看使用的这两个系统方法的源码
public struct Array<Element> {
@inlinable public func compactMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]
@inlinable public func sorted(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows -> [Element]
}
发现compactMap
和sorted
函数都返回一个数组类型,满足链式调用的条件。
可以将上面的链式拆解为
let arr1 = countArray.compactMap { $0 }
let arr2 = arr1.sorted(by: < )
通过示例2可以发现链式的另一个优点: 减少中间变量。
示例4:链式UI的使用
链式UI的介绍
移动端的开发工作离不开对UI的操作(包含UI对象的声明,UI对象的属性配置,UI对象的添加,UI对象的约束布局等操作步骤)。这些操作需要大量的代码来实现,如果对代码书写规范不严格遵守的话,相关代码就有可能分散在文件的各个地方,影响代码的整体结构性和阅读性。我们可以用链式思想来解决这样的问题。
let _ = UILabel()
.adhere(toSuperView: view)
.layout (snapKitMaker: { (make) in
make.top.equalToSuperview().offset(80)
make.centerX.equalToSuperview()
})
.config ({(label) in
label.backgroundColor = UIColor.clear
label.font = UIFont.systemFont(ofSize: 20)
label.textColor = UIColor.darkGray
label.text = "Label"
})
该段代码实现了四个功能:
- 初始化UILabel类型的对象
- 将该对象添加到父视图上
- 给这个对象添加约束布局
- 给这个对象设置属性
通过链式
这种实现方式,把UI的相关的代码都写在一起,方便管理和维护,极大的提升了代码的可读性。
通过命名空间式扩展避免命名冲突
前面我们给这些UIKit的类通过扩展的形式添加了这些方法 adhere
, layout
, config
,万一以后苹果也使用了同样的方法命名,我们就只能改方法的命名,非常不友好。
SnapKit
前几个版本的是通过添加 snp_
前缀的方式用来区分的。
view.snp_makeConstraints { (make) in
}
现在主流的Swift三方库都支持采用命名空间式扩展
view.snp.makeConstraints { (make) in
}
所以我们的链式UI可以改成这样
let _ = UILabel()
.bt.add(toSuperView: testLabel)
.bt.layout ({
$0.center.equalToSuperview()
$0.width.height.equalTo(100)
})
.bt.config(config)
.bt.config ({
$0.backgroundColor = UIColor.green
})
关于链式UI的其他一些说明
我们的方法public func config(_ config: (T) -> Void) -> T
。config 方法接收一个闭包作为参数,所以也可以这样用,增强复用性:
// 写一个配置的闭包
let config = {(label: UILabel) in
label.backgroundColor = UIColor.red
label.font = UIFont.systemFont(ofSize: 14)
label.text = "label"
label.textColor = UIColor.white
}
let testLabel = UILabel()
.bt.add(toSuperView: view)
.bt.layout ({
$0.edges.equalToSuperview()
})
.bt.config(config)
let _ = UILabel()
.bt.add(toSuperView: testLabel)
.bt.layout ({
$0.center.equalToSuperview()
$0.width.height.equalTo(100)
})
.bt.config(config)
// 差异化设置通配属性
.bt.config ({
$0.backgroundColor = UIColor.green
})
用链式UI的方式把UI相关的代码都强制写一起了,是不是为之后的更新代码不便呢❓
// 更新某一个需要更新的属性配置
testLabel.bt.config {
$0.textColor = UIColor.orange
}
// 当然这样也是可以的
testLabel.text = "改变红色为橘色"
链式UI实现源码
public protocol NamespaceWrappable {
associatedtype BTWrapperType
var bt: BTWrapperType { get }
static var bt: BTWrapperType.Type { get }
}
public extension NamespaceWrappable {
var bt: NamespaceWrapper<Self> {
return NamespaceWrapper(value: self)
}
static var bt: NamespaceWrapper<Self>.Type {
return NamespaceWrapper.self
}
}
public struct NamespaceWrapper<T> {
public let wrappedValue: T
public init(value: T) {
self.wrappedValue = value
}
}
import SnapKit
extension NamespaceWrapper where T: UIView {
/// 添加在视图
/// - Parameter toSuperView: 父视图
public func add(toSuperView: UIView) -> T {
toSuperView.addSubview(wrappedValue)
return wrappedValue
}
@discardableResult
public func config(_ config: (T) -> Void) -> T {
config(wrappedValue)
return wrappedValue
}
@discardableResult
public func layout(_ snapKitMaker: (ConstraintMaker) -> Void) -> T {
wrappedValue.snp.makeConstraints { (make) in
snapKitMaker(make)
}
return wrappedValue
}
}
三. 阶段性总结
通过以上的说明和几个简单的示例,发现链式具有以下几个特点:
- 代码简洁
- 高复用性
- 高可读性
- 减少中间变量
四. 链式调用的安全性
先来看一段约束布局
testLabel.snp.makeConstraints { (make) in
make.width.equalTo(view.snp.left)
}
此段约束明显是有问题的,但是在编译期不会报错,只有真正运行到此处才会报错: 布局属性的配对无效.
: 'NSLayoutConstraint for <UILabel>: Invalid pairing of layout attributes.'
往往这种潜在问题都是致命的。接下来我们探究如何避免这种情况?
我们尝试用代码实现一下这个要求:
有句网络名言是这样的“同性才是真爱,异性只为繁殖后代”
实现一个Love
的协议,声明 Man
和 Women
两个类,并遵守该协议。
protocol Love { }
class Man: Love { }
class Women: Love { }
extension Love {
var man: Man {
return Man()
}
var women: Women {
return Women()
}
}
struct Validation {
static func trueLove<T: Love>(left: T, right: T) {
print("存在真爱")
}
}
❎ Cannot invoke 'trueLove' with an argument list of type '(left: Man, right: Women)'
Validation.trueLove(left: man, right: women)
Validation.trueLove(left: man, right: man.women)
✅
Validation.trueLove(left: man, right: man)
虽然中间一个是man
一个是 women
,但是链式的最后一个对象类型是相同的。所以可以通过编译。显然不能满足我们的需求。
❓运行能否成功?答案是肯定的。但是不符合我们的要求。
Validation.trueLove(left: man. man. man, right: man.women.man)
使用泛型约束继续实现
protocol Love { }
class Man<X>: Love { }
class Women<X>: Love { }
extension Love {
var man: Man<Self> {
return Man()
}
var women: Women<Self> {
return Women()
}
}
struct Validation {
static func trueLove<T: Love>(left: T, right: T) {
print("存在真爱")
}
}
最终使用的时候是这样的
let man = Man<Any>()
let women = Women<Any>()
❎
Validation.trueLove(left: man, right: man.women.man)
Validation.trueLove(left: women.man, right: man.women.man)
✅
Validation.trueLove(left: man, right: man)
Validation.trueLove(left: women.man, right: women.man)
可以通过这样的方式,降低程序的错误率,提高代码的安全性。
五. 可选链式调用
1. 概念
可选链式调用是指当前值为可选类型情况下,对当前值执行(获取属性、方法或下标)操作,会出现以下情况
- 如果当前的可选值为nil,调用失败并返回nil;
- 如果当前的可选值有值,调用成功;
多个调用可以连接在一起形成调用链,当其中任意一个节点返回nil,则整个调用链调用失败返回nil。
2. 示例说明
class Person {
var residence: Residence?
}
class Residence {
var rooms = [Room]()
var numberOfRooms: Int {
return rooms.count
}
// 通过下标访问
subscript(i: Int) -> Room {
get {
return rooms[i]
}
set {
rooms[i] = newValue
}
}
// 隐式返回类型Void. -> Void
func printNumberOfRooms() {
print("The number of rooms is \(numberOfRooms)")
}
var address: Address?
}
class Room {
let name: String
init(name: String) { self.name = name }
}
class Address {
var buildingName: String?
var buildingNumber: String?
var street: String?
func buildingIdentifier() -> String? {
if let temp = buildingName {
return temp
}
if let temp1 = buildingNumber, let temp2 = street {
return temp1 + temp2
}
return nil
}
}
- 读取属性
let xiaoMing = Person()
if let roomCount = xiaoMing.residence?.numberOfRooms {
print("xiaoMing's residence has \(roomCount) room(s).")
} else {
print("Unable to retrieve the number of rooms.")
}
// Unable to retrieve the number of rooms.
- 设置属性
// 通过xiaoMing.residence来设定address属性也会失败,因为xiaoMing.residence当前为nil
let address = Address()
address.buildingName = "2.5产业园"
address.buildingNumber = "88号"
address.street = "dongchang Road"
xiaoMing.residence?.address = address
- 通过可选链式调用调用方法
if let _ = xiaoMing.residence?.printNumberOfRooms() {
print("It was possible to print the number of rooms.")
} else {
print("It was not possible to print the number of rooms.")
}
//It was not possible to print the number of rooms.
- 通过可选链式调用访问下标
if let firstRoomName = xiaoMing.residence?[0].name {
print("The first room name is \(firstRoomName).")
} else {
print("Unable to retrieve the first room name.")
}
//Unable to retrieve the first room name.