Swift:4个简化属性设置代码的神奇技巧 【译】

2,297 阅读6分钟

原文地址4 Magic Tricks to Simplify Property Configuration in Swift
原文作者André Gillfrost

本文主演:extensions,closures、泛型、keypaths、下标

当我们试着改善Swift中定义对象的方式的时候,我们设下几个目标,然后尝试从不同角度去实现,最后发现我们可能用力过猛,只是写下一些看起来很傻的代码,但是在这过程中,也许也摸索出了一些心得。

案例

下面就是我们的对照组,使用最原始的UIKit布局代码,大家应该不陌生:

let label = UILabel()
func setupLabel() {
    label.text = "text"
    label.textColor = .blue
}

这么写没什么问题,但如果没有什么代码规范约束的话,很可能这种类型的代码会变得分散,或者出现在你不想看到的地方,应该怎么补救呢?

首要准则

  • 布局配置代码需要统一规范
  • 唯一地完成配置,最好在声明它的时候

第二准则

  • 简洁,因为简洁是智慧的灵魂

1:链式语法

使用扩展方法扩展类型,并返回Self实例,可以让属性配置变得顺滑起来。

这么做的好处是,每个方法的名称都它的属性名一样。这样就没有引入新的东西,也没有增加学习成本,异曲同工。

extension UIView {
    func backgroundColor(_ backgroundColor: UIColor?) -> Self { 
        self.backgroundColor = backgroundColor
        return self
    }
}

这使我们可以在声明一个UIView实例后,立即设置它的背景色,并且仍然能保存对该实例的引用。

let blueView = UIView().backgroundColor(.blue)

需要注意
这个方法返回的类型是Self,如果是对UIView进行的扩展,那么你用UIView子类实例,例如UILabel(),去调用这个方法的时候,返回的仍然是一个UIView类型的实例

为了能返回特定类型的实例,可以再对指定子类进行扩展。

当然,上面被扩展的UIView类型,是扩展链的第一环。 backgroundColor方法扩展了UIView,它所有的子类都可以使用,同时你也可以对UILabel扩展设置text的方法。

extension UILabel {
    func text(_ text: String?) -> Self {
        self.text = text
        return self
    }
}
let label = UILabel().backgroundColor(.blue).text("I'm a label")

有什么收获
这种扩展方法设置属性的一个问题是,它的复用性不是太好,你必须给每一种你需要的属性扩展方法。

此外,初始化实例的时候,设置属性的代码可能会变得很长,但是我认为这不应该被称为缺陷,不如说是feature

我现在就在项目中用了这种方法,看起来很漂亮不是吗?

private let titleLabel = UILabel()
    .lines(0)
    .autoshrink(to: 0.5)
    .fontSize(17)

当然,我们还要尽可能简化冗长的方法名称,并且在一个方法中尽可能合并相关的属性,提升可读性,即使项目新手也可以很快地上手。

当然我不是第一个偶然发现这个想法的人。 你可以在GitHub上搜索 ChainKit 等项目,编写所有这些方法的繁复工作已经完成。 使用Pod安装之后,你就可以愉快地使用了。

2:BlockConfigurable

KotlinSwift有很多相似点,也有一些非常有用的小技巧。

Kotlin有一个apply方法,它提供一个block方法,在block内部可以可以任意调用对象的属性或者给属性赋值,这样做真的很方便。返回值是本身。

这就意味着,你不必在block内去指定接受者,只需像在给自己设置属性那样去操作即可,就像上面链式的扩展方法一样,接收者是从apply方法返回的,所以这些属性在block结束后就会绑定到接受者上面。就像这样:

val dude = Dude().apply {
    name = “Dude”
    age = 48
}

想象一下,我们能在Swift中这么做吗?就像这样:

let view = UIView().apply {
    backgroundColor = .blue
    isHidden = true
}

不能,但是,我们不能就这么放弃。

我们可以试着像那个方向靠,我们可以定义一个带有给属性赋值功能的block,把这个block作为参数传递进来,然后再return self

extension UIView {
    func apply(block: (UIView) -> Void) -> UIView {
        block(self)
        return self
    }
}

调用的时候,这个block可以写为尾随闭包的方式,并通过匿名参数$0代表这个UIView(),这样就很大程度上实现了Kotlin的这个语法糖。

let view = UIView().apply {
    $0.backgroundColor = .blue
    $0.isHidden = true
}

万物皆可apply
我们来让它变得更加通用,定义一个protocol,然后给它一个默认实现,同时限定遵守这个协议的对象是AnyObject类型。

protocol BlockConfigurable {}
extension BlockConfigurable where Self: AnyObject {
    func apply(block: (Self) throws -> Void) rethrows -> Self {
        try block(self)
        return self
    }
}
extension NSObject: BlockConfigurable {}

OK,我们甚至加入了try catch来捕获可能会出现的异常。

进行到这里,所有继承自NSObject的类,包括所有的view,还有框架内绝大部分的对象,都可以使用这个apply方法了。

你可能已经有所耳闻,没错,Then 就是这么做的,但是更细致完善一些,而且它用的是then而不是apply,你可以使用Pod去安装和使用他们。

你以为到这里就结束了?NO!

3:KeypathConfigurable

我还是喜欢第一种扩展backgroundColor的方式,兼备可读性和安全性。我不喜欢所有的代码都需要去明确类型,无论是自己写的还是用的三方工具库。

我想要的是类似泛型的方法,或者类似的方法。这就轮到ReferenceWritableKeypath<Root, Value>上场了。

Swift中,keypath是反斜杠(\)+ 属性 + 点(.)定义的:

\UIView.backgroundColor

这个keypath的具体类型是ReferenceWritableKeypath<UIView, UIColor?>,如果类型已知,可以省略,我们拿\.backgroundColor举例

我们定义一个方法,这个方法可以通过keypath为任意实例设置属性,并且返回实例self本身。

protocol KeypathConfigurable {}
extension KeypathConfigurable where Self: AnyObject {
    func sporting<T>(_ keyPath: ReferenceWritableKeyPath<Self, T>,
                     _ value: T) -> Self {
        self[keyPath: keyPath] = value
        return self
    }
}

现在给一个UIView设置属性就像下面这样

let view = UIView()
    .sporting(\.backgroundColor, .blue)
    .sporting(\.isHidden, true)

是不是比之前好一些呢?
但是可能还不够。 再优化一下!

4:SubscriptKeypathConfigurable

这样一次次通过sporting去设置属性和我们简洁的初衷渐行渐远了,反斜杠(\)+点(.)的方式又很难消除,除非重写Swift(摊手),如果非要这么做,可以试试Kotlinapply方式。

用下标语法替换sporting试试怎么样呢?

protocol SubscriptKeypathConfigurable {}
extension SubscriptKeypathConfigurable where Self: AnyObject {
    subscript<T>(_ keyPath: ReferenceWritableKeyPath<Self, T>, 
             _ value: T) -> Self {
        self[keyPath: keyPath] = value
        return self
    }
}

extension UIView: SubscriptKeypathConfigurable {}

好了,让我们再试试:

let view = UIView()[\.backgroundColor, .blue][\.isHidden, true]
let label = UILabel()[\.backgroundColor, .red][\.text, "label"][\.isHidden, false]

总结:我们都做了什么

我们尝试了一些替代传统方法设置属性的新方式。
包括:extenison、闭包closure、键值keypath、下标Subscript,来使设置属性变得更加简洁,更加地可读和方便维护。

好与坏,各有分说,我贴一段wiki上关于语法糖的一段话:
…things can be expressed more clearly, more concisely, or in an alternative style that some may prefer.