原文:Using tuples as lightweight types in Swift | Swift by Sundell
Swift 的一个真正有趣的功能是能够使用元组(Tuples)创建轻量级容器。这个概念很简单 -- 元组让你轻松地将任何数量的对象或值组合在一起,而无需创建一个新类型。但是,尽管这是一个简单的概念,它却开启了一些非常酷的机会,无论是在 API 设计方面还是在结构化代码方面。
在标准库中,元组被用于形成 (key, value)
类型的键值对,当迭代一个字典时,或者当返回插入一个 Set
的结果时(就像我们上周在Swift 中集合的力量中使用的那样)。本周,让我们来看看我们如何在自己的代码中使用元组,以及它们使我们能够使用的一些技术。
轻量级类型
描述元组的一种方法是,它们是轻量的、内联的类型。如果你有两个值 -- 比方说一个 title
和一个subtitle
-- 你可以快速地将它们组合成一个新的类型,在你要使用它的地方完全内联,就像这样:
swift
class TextView: UIView {
func render(_ texts: (title: String, subtitle: String)) {
titleLabel.text = texts.title
subtitleLabel.text = texts.subtitle
}
}
上述方法的一个替代方案是创建一个 struct
。虽然在你会在多个地方使用这种类型对的情况下,这可能是更好的选择,但能够在你实际使用它的地方保持内联的类型定义,确实有助于保持事情的简单。它也使得添加额外的属性像改变函数签名一样简单:
swift
class TextView: UIView {
func render(_ texts: (title: String, subtitle: String, description: String)) {
titleLabel.text = texts.title
subtitleLabel.text = texts.subtitle
descriptionLabel.text = texts.description
}
}
使用类似上述的方法可能看起来微不足道,但我个人发现,如果我可以在同一个地方输入东西,而不是在代码库中的多个地方跳来跳去来做这样一个简单的改变,这确实对生产力有很大影响。
不过,上述方法的一个缺点是,如果属性列表开始增加,事情就会变得有点混乱。解决这个问题的一个方法是使用一个 typealias
来为元组创建一个轻量级的类型定义,同时仍然让事情保持简单:
swift
class TextView: UIView {
// 使用 typealias 为元组创建轻量级类型定义
typealias Texts = (title: String, subtitle: String, description: String)
func render(_ texts: Texts) {
titleLabel.text = texts.title
subtitleLabel.text = texts.subtitle
descriptionLabel.text = texts.description
}
}
做到这一点后,如果我们将来想把元组提取成一个明确的类型 -- 比如一个 struct
-- 也会变得超级容易,因为我们的代码现在是用 Texts
来引用它。
简化的调用栈点
关于元组的另一个很酷的事情是它们对某个 API 的调用栈点的影响。尽管元组可以有标签,但在创建一个实例时,你总是可以自由地忽略这些标签。这可以帮助使调用栈点看起来非常漂亮和干净,例如在处理矢量类型,如坐标时。
比方说,我们的应用程序有一个在基于块状的地图上存储位置的模型,我们选择使用一个元组来定义地图上的坐标,像这样:
swift
struct Map {
private let width: Int
private var tiles: [Tile]
subscript(_ coordinate: (x: Int, y: Int)) -> Tile {
return tiles[coordinate.x + coordinate.y * width]
}
}
由于我们使用了一个元组,我们现在可以选择在创建坐标时包含或省略 x
和 y
标签:
swift
let tileA = map[(x: 1, y: 0)]
let tileB = map[(2, 1)]
等同性
在检查多个值是否相等时,元组也是非常有用的。尽管它们不符合 Equatable
协议(或任何协议),但 Swift 标准库定义了 ==
重载,用于包含本身是相等的值的元组。
假设我们正在构建一个视图控制器,让用户在给定范围内搜索其他用户(例如,他们可以选择只在他们的朋友中搜索)。由于我们不想浪费资源去搜索已经被显示出来的结果,我们可以很容易地使用一个元组来跟踪当前的搜索标准,并验证它们自上次搜索后是否真的发生了变化,就像这样:
swift
class UserSearchViewController: UIViewController {
enum Scope {
case friends
case favorites
case all
}
// 这里将用户的搜索关键字、搜索区域声明成一个元组类型
private var currentCriteria: (query: String, scope: Scope)?
func searchForUsers(matching query: String, in scope: Scope) {
if let criteria = currentCriteria {
// If the search critera matches what is already rendered, we can
// return early without having to perform an actual search
// 如果搜索标准与已经呈现的内容相符,我们可以提前返回,而不需要进行实际的搜索
guard (query, scope) != criteria else {
return
}
}
currentCriteria = (query, scope)
// Perform search
...
}
}
不需要定义额外的类型,也不需要编写任何 Equatable
实现(尽管希望这很快就会成为过去,因为 Swift 很快就会开始为我们合成大部分的实现🎉)。
与一等类函数结合
将元组与 Swift 支持一等类函数的事实相结合,可以让我们做一些非常有趣的事情。事实证明,任何闭包的参数列表实际上都可以用一个元组来描述,而且由于一等类函数的存在,所有的函数也都是闭包,我们实际上可以用一个元组来向一个函数传递参数。我们所要做的就是让 Swift 把一个函数当作一个闭包。为了做到这一点,我们可以定义一个调用函数,该函数接收任何函数,并将其所需的参数应用于它,像这样:
swift
func call<Input, Output>(_ function: (Input) -> Output, with input: Input) -> Output {
return function(input)
}
现在我们假设要实例化一组视图类,它们都可以使用相同类型的参数列表进行初始化:
swift
class HeaderView: UIView {
init(font: UIFont, textColor: UIColor) {
...
}
}
class PromotionView: UIView {
init(font: UIFont, textColor: UIColor) {
...
}
}
class ProfileView: UIView {
init(font: UIFont, textColor: UIColor) {
...
}
}
使用我们的 call
函数,我们现在可以简单地应用一个包含预期参数的元组(在这种情况下,我们想使用创建视图的样式),将每个视图的初始化器作为一个函数传递,并使用我们的 style
元组调用它。
swift
let styles = (UIFont.systemFont(ofSize: 20), UIColor.red)
let headerView = call(HeaderView.init, with: styles)
let promotionView = call(PromotionView.init, with: styles)
let profileView = call(ProfileView.init, with: styles)
上面的内容有点疯狂,但它非常酷! 😀
总结
Swift 中的元组很简单,但非常强大。我们不仅可以用它们来快速定义内联类型,还可以用它们来使检查多个值是否相等这样的任务变得简单得多。最后,结合一等类函数,我们可以使用元组来将参数列表作为一个值来传递,这真的很有趣。
就像其他 Swift 功能一样,元组与更明确定义的类型(如类和结构)相比有不同的权衡。它们更容易定义,而且非常轻量级,但因此它们不能做一些事情,比如存储弱引用或在其中包含任何类型的逻辑(比如拥有方法)。一方面,这是一个缺点,但它也有助于在处理更简单的数据容器时保持简单性。
你是怎么想的?你目前是否在你的代码中使用元组,或者是你要尝试的东西?让我知道,以及你可能有的任何其他问题、评论或反馈,请在 Twitter 上 @johnsundell。
谢谢你的阅读!🚀