Swift GYB

Mattt撰写、 Bei Li翻译、 发布于

「boilerplate」这个词可以追溯到印刷媒体早期。本地小报需要内容来填满他们报纸的版面,但又通常没有足够的内容,所以很多本地小报通过大型印刷集团来获取稳定的内容,来填充报纸的最后几页。这些内容通常由预制的印板(plate)提供,这些印板长的很像曾经用来制作锅炉(boiler)的卷起来的钢板。因此有了「boiler-plate」这个词语。

经过转喻,这些内容本身被称作「boilerplate」,并且这个概念被拓展到包括标准化和公式化的文本。这些文本出现在合同和套用信函(form letter)里,以及与本周 NSHipster 文章最相关的东西——代码。


并不是所有的代码都那么光鲜亮丽,其实很多工作都是通过底层的 boilerplate 来工作的。

Swift 标准库也是这样,它有诸如符号整数(Int8, Int16, Int32, Int64)这样的类型家族,其中各个类型的实现除大小之外没有其他不同。

拷贝粘贴可以作为一次性的解决方案(假设你第一次就做的没有错误),但这种方法无法继续维护。每当你想修改这些派生的实现,都会有引入细微不一致的风险,久而久之导致这些实现变得不相同——有一点类似于地球上生命的多样性是因为基因随机突变导致的一样。

从 C++ 模板和 Lisp 宏到 eval 和 C 预处理器指令,不同的语言使用不同的技术来应对这个问题。

Swift 没有宏系统,并且因为标准库本身是使用 Swift 编写的,所以它也不能利用 C++ 元编程的能力。因此,Swift 的维护者们使用一个叫作 gyb.py 的 Python 脚本和一些模板标签来生成代码。

GYB 是「Generate Your Boilerplate」的缩写,参考了另一个 Python 工具 GYP 即「Generate Your Projects」。

GYB 是如何工作的

GYB 是一个允许你使用 Python 代码来做变量替换和流程控制的轻量模板系统:

  • %{ <#code#> } 执行一段 Python 代码
  • % <#code#>: ... % end 进行流程控制
  • ${ <#code#> } 会被替换为表达式的结果

其他输入的文本则不会改变。

Codable.swift.gyb 是 GYB 一个很好的例子。在文件的顶部,基础 Codable 类型被赋值给一个实例变量:

%{
codable_types = ['Bool', 'String', 'Double', 'Float',
                 'Int', 'Int8', 'Int16', 'Int32', 'Int64',
                 'UInt', 'UInt8', 'UInt16', 'UInt32', 'UInt64']
}%

之后,在 SingleValueEncodingContainer 的实现中,这些类型被用来循环生成协议要求的方法声明:

% for type in codable_types:
  mutating func encode(_ value: ${type}, forKey key: Key) throws
% end

执行这个 GYB 模板会得到下面这些声明:

mutating func encode(_ value: Bool) throws
mutating func encode(_ value: String) throws
mutating func encode(_ value: Double) throws
mutating func encode(_ value: Float) throws
mutating func encode(_ value: Int) throws
mutating func encode(_ value: Int8) throws
mutating func encode(_ value: Int16) throws
mutating func encode(_ value: Int32) throws
mutating func encode(_ value: Int64) throws
mutating func encode(_ value: UInt) throws
mutating func encode(_ value: UInt8) throws
mutating func encode(_ value: UInt16) throws
mutating func encode(_ value: UInt32) throws
mutating func encode(_ value: UInt64) throws

这个模式的使用贯穿整个文件,用来给像 encode(_:forKey:)decode(_:forKey:)decodeIfPresent(_:forKey:) 这样的方法生成相似、公式化的声明。GYB 减少了几千行 boilerplate 代码:

$ wc -l Codable.swift.gyb
2183 Codable.swift.gyb
$ wc -l Codable.swift
5790 Codable.swift

注意:有效的 GYB 模板不一定能生成有效的 Swift 代码。如果在派生的文件中出现编译错误,可能将很难查明根本原因。

在 Xcode 中使用 GYB

GYB 并不是 Xcode 标准工具链的一部分,所以你并不能在 xcrun 里找到它。你可以下载源码并使用 chmod 命令使 gyb 成为可执行文件(macOS 默认安装的 Python 应该是可以运行 gyb 的):

$ wget https://github.com/apple/swift/raw/master/utils/gyb
$ wget https://github.com/apple/swift/raw/master/utils/gyb.py
$ chmod +x gyb

将这些命令放置到能被你的 Xcode 项目访问到,但与源代码文件不同的地方。比如说项目根目录下一个叫 Vendor 的目录下。

在 Xcode 中,点击 project navigator 中蓝色的项目文件图标,选择项目中正在使用的 target,并去到「Build Phases」面板。在面板顶部,你可以找到一个可以点击的 + 符号,用来增加新的编译阶段。选择「Add New Run Script Phase」,并将下面的代码输入编辑器:

find . -name '*.gyb' |                                               \
    while read file; do                                              \
        ./path/to/gyb --line-directive '' -o "${file%.gyb}" "$file"; \
    done

请确保 GYB 编译阶段放置在「Compile Sources」之前。

现在当你编译你的项目时,任何文件扩展名为 .swift.gyb 的文件会被 GYB 执行并产出 .swift 文件,之后与项目中其他的代码一起编译。

何时使用 GYB

和其他工具一样,知道何时使用 GYB 与知道如何使用 GYB 同样重要。下面有一些你可能会想要使用 GYB 的例子。

生成公式化代码

你是否为集合或者序列中的元素复制粘贴相同的代码?for-in 循环和变量替换可以解决这个问题。

和之前在 Codable 的例子里看到的一样,你可以在 GYB 模板文件的顶部声明一个集合,然后迭代这个集合来生成类型、属性或者方法声明:

%{ abilities = ['strength', 'dexterity', 'constitution',
                'intelligence', 'wisdom', 'charisma']
}
class Character {
  var name: String

% for ability in abilities:
  var ${type}: Int
% end
}

大量的重复代码也常常意味着或许能有更好的解决方法。像协议扩展和泛型这些语言内置的功能可以消灭大量的重复代码,注意是否可以使用这些功能而不是一昧的使用 GYB。

从数据派生代码

你是否在根据数据源来编写代码?那么尝试将 GYB 加入开发过程吧!

GYB 文件可以导入像 jsonxmlcsv 这些 Python 包,你几乎可以解析任何你遇到的文件:

%{ import csv }
% with open('path/to/file.csv') as file:
    % for row in csv.DictReader(file):

如果你想看看这方面的实践,看看 Currencies.swift.gyb,它给 ISO 4217 标准定义的每个货币生成对应的 Swift 枚举类型。

为了保持编译速度和确定性,应该将数据下载到文件并放入版本控制,而不是在 GYB 文件中执行 HTTP 请求或者数据库查询。

代码生成让代码和相关标准之间保持同步变得简单。只需要更新数据文件再重新运行 GYB 就可以了。


Swift 最近通过编译器合成代码减少了很多 boilerplate 代码, 像 4.0 中的 EncodableDecodable,4.1 中的 EquatableHashable,4.2 中的 CaseIterable

与此同时,对于其他代码来说,GYB 是一个非常实用的代码生成工具。

Sourcery 是社区中另一个好用的工具,它让你可以使用 Swift(通过 Stencil)而不是 Python 来编写模板。

「Don’t Repeat Yourself」可能是编程中的美德,但是有些时候你需要重复一些事情来完成工作。当你需要重复的时候,你将会感谢有像 GYB 这样的工具来帮助你。

下一篇文章

NSIndexSet (以及它的可修改子类, NSMutableIndexSet) 是一个排好序的,无重复元素的整数集合。它看上去有点像 支持离散整数的 NSRange .它能用于快速查找特定范围的值的索引,也能用于快速计算交集, 同时,Foundation collection class 提供了很多好用的方法,方便你使用 NSIndexSet.