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]
}

发现compactMapsorted函数都返回一个数组类型,满足链式调用的条件。

可以将上面的链式拆解为

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的协议,声明 ManWomen 两个类,并遵守该协议。

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.

六. 参考声明

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容