在 Swift 中使用元组作为轻量级类型

原文: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]
    }
}

由于我们使用了一个元组,我们现在可以选择在创建坐标时包含或省略 xy 标签:

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。

谢谢你的阅读!🚀

相关推荐
前端爆冲8 分钟前
项目中无用export的检测方案
前端
热爱编程的小曾36 分钟前
sqli-labs靶场 less 8
前端·数据库·less
gongzemin1 小时前
React 和 Vue3 在事件传递的区别
前端·vue.js·react.js
Apifox1 小时前
如何在 Apifox 中通过 Runner 运行包含云端数据库连接配置的测试场景
前端·后端·ci/cd
树上有只程序猿1 小时前
后端思维之高并发处理方案
前端
庸俗今天不摸鱼2 小时前
【万字总结】前端全方位性能优化指南(十)——自适应优化系统、遗传算法调参、Service Worker智能降级方案
前端·性能优化·webassembly
黄毛火烧雪下2 小时前
React Context API 用于在组件树中共享全局状态
前端·javascript·react.js
Apifox2 小时前
如何在 Apifox 中通过 CLI 运行包含云端数据库连接配置的测试场景
前端·后端·程序员
一张假钞2 小时前
Firefox默认在新标签页打开收藏栏链接
前端·firefox
高达可以过山车不行2 小时前
Firefox账号同步书签不一致(火狐浏览器书签同步不一致)
前端·firefox