Swift基础知识介绍:`Equatable`

305 阅读7分钟

检查两个对象或数值是否被认为是相等的,这无疑是所有编程中最常执行的操作之一。因此,在这篇文章中,让我们看看Swift是如何对平等的概念进行建模的,以及该模型在值和引用类型之间是如何变化的。

Swift实现平等的一个最有趣的方面是,它是以一种非常面向协议的方式完成的--这意味着任何类型都可以通过符合Equatable 协议而成为可平等的,这可以像这样做:

struct Article: Equatable {
    static func ==(lhs: Self, rhs: Self) -> Bool {
        lhs.title == rhs.title && lhs.body == rhs.body
    }

    var title: String
    var body: String
}

在上面的例子中,我们符合Equatable 的方式是通过实现== 操作符的重载,它接受两个要比较的值(lhs ,左边的值,和rhs ,右边的值),然后它返回一个布尔结果,说明这两个值是否应该被视为相等。

不过好消息是,我们通常不需要自己编写这些类型的== 操作符重载,因为只要一个类型的存储属性本身都是Equatable ,编译器就能自动合成这种实现。因此,在上述Article 类型的情况下,我们实际上可以删除我们的手动平等检查代码,只需让该类型看起来像这样:

struct Article: Equatable {
    var title: String
    var body: String
}

事实上,Swift的平等性检查是如此的面向协议,这也让我们在处理通用类型时有了很大的权力。例如,一个符合Equatable 的值的集合(如ArraySet )也会自动被认为是可等价的--而不需要我们编写任何额外代码:

let latestArticles = [
    Article(
        title: "Writing testable code when using SwiftUI",
        body: "..."
    ),
    Article(title: "Combining protocols in Swift", body: "...")
]

let basicsArticles = [
    Article(title: "Loops", body: "..."),
    Article(title: "Availability checks", body: "...")
]

if latestArticles == basicsArticles {
    ...
}

这些类型的集合平等性检查的工作方式是通过Swift的*条件符合性功能*实现的,该功能使一个类型只有在满足某些条件时才符合特定的协议。例如,这里是Swift的Array 类型如何符合Equatable ,只有当被存储在给定数组中的元素也反过来符合Equatable--这就是使我们有可能检查两个Article 数组是否被视为相等的原因:

extension Array where Element: Equatable {
    ...
}

由于上述逻辑没有被硬编码到编译器本身,如果我们想让我们自己的通用类型也有条件地相等,我们也可以利用完全相同的基于条件符合性的技术。例如,我们的代码库可能包括某种形式的Group 类型,可以用来标记一组相关的值:

struct Group<Value> {
    var label: String
    var values: [Value]
}

为了使该Group 类型在用于存储Equatable 值时符合Equatable ,我们只需编写下面的空扩展,它看起来与我们上面看到的Array 扩展几乎相同:

extension Group: Equatable where Value: Equatable {}

有了上述内容,我们现在可以检查两个基于ArticleGroup 值是否相等,就像我们使用数组时一样:

let latestArticles = Group(
    label: "Latest",
    values: [
        Article(
            title: "Writing testable code when using SwiftUI",
            body: "..."
        ),
        Article(title: "Combining protocols in Swift", body: "...")
    ]
)

let basicsArticles = Group(
    label: "Basics",
    values: [
        Article(title: "Loops", body: "..."),
        Article(title: "Availability checks", body: "...")
    ]
)

if latestArticles == basicsArticles {
    ...
}

就像集合一样,只要Swift图元的存储值都符合Equatable ,也可以对其进行平等检查:

let latestArticles = (
    first: Article(
        title: "Writing testable code when using SwiftUI",
        body: "..."
    ),
    second: Article(title: "Combining protocols in Swift", body: "...")
)

let basicsArticles = (
    first: Article(title: "Loops", body: "..."),
    second: Article(title: "Availability checks", body: "...")
)

if latestArticles == basicsArticles {
    ...
}

然而,包含上述那种可等价图元的集合并不自动符合Equatable 。因此,如果我们把上述两个图元放到两个相同的数组中,那么这些就不会被认为是可等价的:

let firstArray = [latestArticles, basicsArticles]
let secondArray = [latestArticles, basicsArticles]

// Compiler error: Type '(first: Article, second: Article)'
// cannot conform to 'Equatable':
if firstArray == secondArray {
    ...
}

上面的方法不能工作(至少不是开箱即用)的原因是--就像发出的编译器信息所暗示的那样--图元不能符合协议,这意味着我们前面看了一下的Equatable-conformingArray 的扩展不会生效。

不过,有一个方法可以让上述情况生效,虽然我意识到下面的通用代码可能不属于*"基础知识 "这篇文章,但*我仍然认为它值得快速浏览一下--因为它说明了Swift的等价检查是多么灵活,而且我们不只是限于实现一个单一的== 重载,以符合Equatable

因此,如果我们添加另一个自定义的== 重载,专门用于包含可等价的两元素图元的数组,那么上面的代码样本实际上就能成功编译了:

extension Array {
    // This '==' overload will be used specifically when two
    // arrays containing two-element tuples are being compared:
    static func ==<A: Equatable, B: Equatable>(
        lhs: Self,
        rhs: Self
    ) -> Bool where Element == (A, B) {
        // First, we verify that the two arrays that are being
        // compared contain the same amount of elements:
        guard lhs.count == rhs.count else {
            return false
        }

        // We then "zip" the two arrays, which will give us
        // a collection where each element contains one element
        // from each array, and we then check that each of those
        // elements pass a standard equality check:
        return zip(lhs, rhs).allSatisfy(==)
    }
}

上面我们还可以看到Swift操作符是如何作为函数传递的,因为我们能够直接将== 传递给我们对allSatisfy 的调用。

到目前为止,我们一直在关注值类型(如结构)在检查平等性时的表现,但引用类型呢?例如,假设我们现在决定将之前的Article 结构变成一个类,这将对其Equatable 的实现产生什么影响?

class Article: Equatable {
    var title: String
    var body: String
    
    init(title: String, body: String) {
        self.title = title
        self.body = body
    }
}

在进行上述改变时,我们会注意到的第一件事是,编译器不再能够自动合成我们的类型的Equatable 的一致性--因为该功能仅限于值类型。因此,如果我们希望我们的Article 类型仍然是一个类,那么我们就必须手动实现Equatable 所要求的== 重载,就像我们在本文开头所做的一样:

class Article: Equatable {
    static func ==(lhs: Article, rhs: Article) -> Bool {
        lhs.title == rhs.title && lhs.body == rhs.body
    }

    var title: String
    var body: String

    init(title: String, body: String) {
        self.title = title
        self.body = body
    }
}

然而,作为任何一种基于Objective-C的类的子类,确实从NSObject (它是几乎所有Objective-C类的根基类)继承了一个默认的Equatable 实现。因此,如果我们把我们的Article 类变成一个NSObject 子类,那么它实际上就变成了Equatable ,而不需要严格要求我们实现一个自定义的== 重载:

class Article: NSObject {
    var title: String
    var body: String

    init(title: String, body: String) {
        self.title = title
        self.body = body
        super.init()
    }
}

虽然使用上述子类技术来避免编写自定义的平等性检查代码可能很诱人,但必须指出的是,默认的Objective-C提供的Equatable 实现唯一会做的事情是检查两个类是否是同一个实例--而不是它们是否包含相同的数据。因此,即使下面两个Article 实例具有相同的titlebody ,在使用上述基于NSObject 的方法时,它们也不会被认为是相等的。

let articleA = Article(title: "Title", body: "Body")
let articleB = Article(title: "Title", body: "Body")
print(articleA == articleB) // false

不过,执行这些类型的实例检查可能真的很有用--因为有时我们可能希望能够检查两个基于类的引用是否指向同一个底层实例。不过我们不需要我们的类继承自NSObject 来做到这一点,因为我们可以使用 Swift 内置的三等分操作符=== 来在任何两个引用之间进行这样的检查:

let articleA = Article(title: "Title", body: "Body")
let articleB = articleA
print(articleA === articleB) // true

至此,我相信我们已经涵盖了所有关于Swift中平等的基本原理--对于值和对象,使用自定义或自动生成的实现,以及如何使泛型成为有条件的平等的。 谢谢你的阅读!