Swift中的相等

212 阅读6分钟

Swift中的相等

hudson 译 原文

检查两个对象或值是否被认为相等绝对是所有编程中最常见的操作之一。 因此,在本文中,让我们看看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
}

在上述示例中,通过重载运算符==, 使Article符合Equatable协议 。该运算符接受要比较的两个值(lhs,左侧值和rhs,右侧值),然后返回一个关于这两个值是否应被视为相等的布尔结果。

通常不必自己编写这些==运算符重载代码,因为每当类型的存储属性本身都是Equatable时,编译器就能够自动合成此类实现。因此,对上述Article类型 , 实际上可以删除手动相等检查代码,并简单地使该类型看起来像这样:

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

Swift的相等检查如此面向协议,这一事实在处理范型类型 时给我们提供了许多强大的功能。例如,符合Equatable的集合(如Array数组或Set)也自动被认为是相等的——我们不需要任何额外的代码:

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

然而,包含上述可相等元组的集合并不自动符合Equable。因此,如果把上述两个元组放入两个相同的数组中,那么这些元组将不被认为是Equable

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

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

上述内容不起作用的原因(至少不是开箱即用)是因为——就像发出的编译器消息暗示的那样——元组不能符合协议,这意味着之前使Array遵从Equatable的扩展不会生效。

不过,有一种方法可以修补上述问题,虽然我意识到以下范型代码可能不属于标记为“基础”的文章,但我仍然认为它值得快速查看——因为它说明了Swift的等式检查有多灵活,而且我们不仅限于实现单个==重载以符合Equatable

我们需要为元素为Equable元组的数组添加另一个自定义==重载 ,那么上述代码示例实际上将成功编译:

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

在执行上述更改时,值得注意到的第一件事是,编译器不再能够自动合成类型的Equable——因为该功能仅限于值类型。因此,如果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子类,那么它实际上将变成Equable,而无需严格要求实现自定义==重载:

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实例具有相同的title标题和body正文,但在使用上述基于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中识别对象

小结

本文介绍了关于相等在Swift中如何工作的所有基础知识——对于值对象和引用对象,使用自定义或自动生成的实现,以及如何使泛型有条件地相等。如果您有任何问题、评论或反馈,请随时通过Twitter电子邮件联系。

谢谢您的阅读!