Equality

检查两个对象或值是否被视为equal绝对是所有编程中最常用的操作之一。因此,在本文中,让我们看看Swift如何建模平等概念,以及该模型在值和引用类型之间如何变化。

Swift实现equality的一个最有趣的方面是,这一切都是以非常面向协议的方式完成的——这意味着任何类型都可以通过遵守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的值(如数组或集合)的集合也会自动被视为equatable值——而不需要我们提供任何额外的代码:

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的条件一致性功能,该功能使类型只有在满足某些条件时才能符合特定协议。例如,以下是只有当存储在给定数组中的元素也符合Equatable时,Swift的数组类型才符合Equatable——这使我们能够检查两个Article数组是否被认为是相等的原因:

extension Array where Element: Equatable {
    ...
}

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

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

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

extension Group: Equatable where Value: Equatable {}

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

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 {
    ...
}

上述功能不起作用(至少不是开箱即用)的原因是——正如发布的编译器消息所暗示的那样——元组不符合协议,这意味着我们之前查看的符合可等的数组扩展将不会生效。

然而,有一种方法可以使上述内容发挥作用——因为它说明了Swift的等式检查有多灵活,而且我们不仅仅是为了符合Equatable而实现单个==重载。

因此,如果我们添加另一个,自定义==重载,特别是对于包含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的调用。

到目前为止,我们一直在关注值类型(如结构体)在检查相等时的行为,但引用类型呢?例如,假设我们现在决定将之前的文章结构体转换为类,这将如何影响其Equatable实现?

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

在执行上述更改时,我们会注意到的第一件事是,编译器不再能够自动合成我们类型的Equatable——因为该功能仅限于值类型。因此,如果我们希望我们的文章类型用一个类实现,那么我们必须手动实现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实现。因此,如果我们要将文章类作为NSObject子类,那么它实际上会变得可等,而不会严格要求我们实现自定义==重载:

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实现只会检查两个类是否是相同的实例——而不是如果它们包含相同的数据。因此,即使以下两个文章实例具有相同的标题和正文,但在使用上述基于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

有了这个,我相信我们已经涵盖了equality如何在Swift中工作的所有基础知识——对于值和对象,使用自定义或自动生成的实现,以及如何使泛型有条件地等同。