Swift 类型中的 Equality, Identity 和 Hashing

原文:Equality, Identity, and Hashing with Swift Types

编写高效且易于测试的代码,减少错误的发生。

在开始之前,让我们先来回答以下问题。

什么是类型?

这个问题的答案相当简单------**类型是一个相关数据的集合,封装了某些概念和原则。**一个类型可以是抽象的,也可以是具体的,它定义了一组我们可以对其进行的有效操作,最后,一个类型明确划分了内部和外部的界限(封装性的体现)。

类型是一种非常好的文档形式,比指导性的注释好得多。类型的设计可以保证代码的安全和行为的正确性,由于类型的工作是安全的,所以在编译时,你可能会在工作中遇到很多编译器错误。但这是件好事,因为这使我们能够在大多数问题进入生产应用之前就将其修复。我鼓励你把这些当成编译器在我们出错时生成的待办事项清单。

类型也使我们能够编写易于测试的代码。例如,让我们以下面两个函数为例:

swift 复制代码
func getAddress() -> String
swift 复制代码
func getAddress() -> Any

以上所给的函数中,哪个更容易测试?第二个函数提供了更多的灵活性,因为它允许我们返回 Any 类型,但是在测试方法的正确性时,同样的灵活性被证明是具有挑战性的。如果我们看一下第一个函数,我们可以确定它将返回一个String类型。

仅仅通过观察第一个函数,我们就可以看到它每次都会返回一个String,但是第二个函数就不一样了,这就使得它更难测试。

Equality 和 Identity / 等同性和唯一性

我们先借助下面的例子来探讨 "Equality":

swift 复制代码
struct EmployeeID {
  private(set) var id: Int

    init?(_ raw: Int) {
        guard raw > 1000 else {
            return nil
        }
        id = raw
    }
}

我们有一个 EmployeeID 类型。

我们有一个 EmployeeID 的可失败初始化器,可以防止创建一个小于 4 位数的 EmployeeID

为了方便测试 EmployeeID 类型,让我们增加 Equatable 协议一致性,看看编译器对现有代码的反应:

swift 复制代码
struct EmployeeID : Equatable {
  // Same code as above
}

let employeeIDOne = EmployeeID(1001)
let employeeIDTwo = EmployeeID(1001)
print("Both EmployeeID's are \(employeeIDOne == employeeIDTwo ? "equal" : "unequal")")

我们可以看到,编译器并没有抱怨,这是因为 EmployeeID 是一个值类型(ValueType),id 是一个 Int 类型。编译器隐含地生成了一致性要求,因此,执行上面的代码会在控制台中打印以下内容。

让我们借助于下面的例子来研究引用类型的 Equality 行为

swift 复制代码
class Employee {
  var id: EmployeeID
  var name: String
  
  init(id: EmployeeID, name: String) {
    self.id = id
    self.name = name
  }
}

我们有一个 Employee 类,并注意到它需要一个 EmployeeID 类型。在这种情况下,Employee 是一个依赖类型 ,如果没有一个有效的 EmployeeID 或者至少是一个超过四位数的 EmployeeID,就不可能创建一个 Employee。这些都是强有力的保证。

现在,如果我们像对待 EmployeeID 类型那样使 Employee 类型遵循 Equatable 协议,你会注意到编译器会直接开始发出警告,信息如下:"Type 'Employee' does not conform to protocol 'Equatable'"。这其中的一个原因是:

引用类型可能会形成一个对象的引用循环,并创造一个检查等同性的无限循环。Codable,它确实生成了一个自动的 Equatable,目前也有这个问题,如果有一个循环,它将创建一个无限的循环。

解决上述问题的方法非常简单------只需在代码中实现 "==",如下所示:

swift 复制代码
class Employee : Equatable {
    static func == (lhs: Employee, rhs: Employee) -> Bool {
        return lhs.id == rhs.id &&
            lhs.name == rhs.name
    }
    // Same as above.
}

标准的实现是将左边的所有属性与右边的进行比较。

引用类型除了 Equality 之外还有 Identity 的概念,让我们借助下面的代码例子看看这两个概念有什么不同:

swift 复制代码
guard let employeeId = EmployeeID(1001) else {
    fatalError("Not a valid Employee ID")
}
let firstEmployee = Employee(id: employeeId, name: "EmployeeOne")
let copyOfFirstEmployee = Employee(id: employeeId, name: "EmployeeOne")
print("Both Employees are \(firstEmployee == copyOfFirstEmployee ? "Equal" : "Unequal")")
print("Both Employees are \(firstEmployee === copyOfFirstEmployee ? "Identical" : "Unidentical")")

在这里,我们用相同的 EmployeeId 创建了两个相似的 Employee 副本,然后我们比较了这两个 Employee 对象,看它们是否相等。我们还用 "==" 和 "===" 运算符来检查它们是否相同。执行上述代码的结果是在控制台中打印出以下内容:

"===" 检查两个对象是否指向相同的引用 ,由于两个引用都指向内存中相等但不同的位置,比较这些对象身份的结果是错误的,即 firstEmployeecopyOfFirstEmployee 是相等的,但它们在内存中并不指向同一个对象。

如果我们把 firstEmployee 分配给一个新的变量,然后检查这两个变量的 Identity,会发生什么?

swift 复制代码
let refereceToFirstEmployee = firstEmployee
print("Both References are \(refereceToFirstEmployee === firstEmployee ? "Identical" : "Unidentical")")

执行这段代码的结果是在控制台打印出以下内容:

证明这一次,两个对象现在指的是内存中的同一个位置。

我们可以灵活地定义 "==",但最好是保持简单,比较所有的属性,只要我们不产生无限循环。

添加 Equatable 一致性使模型更可用,更容易测试。

Hashable

Hashable 是一种比较对象的更强大的方式。当我们需要在 DictionarySet 中进行快速查找时,它很有用。

Set 中的元素或 Dictionary 中的键必须是可哈希的;否则,编译器会开始抱怨。

另外,Hashable 也需要 Equatable 一致性 。让我们看看它是如何工作的。假设我们有一个包含 7 个可哈希对象的 Set,如下图所示:

Set 中的每个对象都会返回一个看似随机但实际上并非随机的数字,称为 Hash 值。这个 Hash 值然后被用来为底层的 Set 存储生成一个槽号。

槽数 = 集合中单个元素的哈希值 % 集合中元素的总数。

例如,假设元素 0 返回的哈希值是 1234567。这个元素的槽位数是 1234567 乘以 7=5。

不同的实例产生不同的哈希值,从而产生不同的槽号,但有时甚至不同的哈希值可能导致相同的槽号,从而导致碰撞,如图片中的元素 3-5 所示。

碰撞会减慢性能,这就是为自定义类型建立有效的散列机制的原因。在我们上面的例子中,当涉及到查找元素 0 时,如果散列值和槽号生成为唯一的,这可以在恒定时间内发生,即 O (1)。让我们看看 hashable 是如何影响现有代码的。

在上面的代码例子中,为了让 EmployeeID 成为可哈希类型,我们需要做的就是让它符合 Hashable 而不是 Equatable,Swift 会在后台为我们做必要的调整,因为 EmployeeID 里面的存储值是一个 IntInt 本质上是可散列的。

但同样的事情并不直接适用于 Employee 类型,因为它是一个引用类型。正因为如此,当我们试图使其成为可哈希类型的时候,编译器就开始抱怨了,如下图所示。

使 Employee 可哈希

Swift 的最新迭代使得正确实现 Hashing 变得非常直观和清晰。让我们来看看这段代码:

swift 复制代码
class Employee : Hashable {
  
    // Same as above
    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(name)
    }
}

我们首先实现 hash(into hasher: inout Hasher) 方法,并利用传入的 hasher 来哈希我们所有的属性。如代码所示,Hasher 使用了名为 combine 的方法,并利用可哈希的子对象来生成哈希值。

一个最佳实践是在 hash(into:) 方法中使用我们在 == 中使用的所有属性。

如果两个对象是相等的,那么它们必须返回相同的哈希值。否则,在 DictionarySet 中的查找将不起作用。而反过来就不对了,因为两个不同的对象可以返回相同的哈希值,我们最终会出现碰撞。散列器试图创建密码学上正确的散列值。

好的哈希值是必须的。否则,想象一下,如果一个恶意的黑客能够通过为字典内的键注入相同的哈希值来造成碰撞,会发生什么?提示:☠️☠️☠️**DENIAL OF SERVICE!!!**☠️☠️☠️

关于其他的更新,你可以在 Twitter 上关注我,我的 twitter@NavRudraSambyal

谢谢你的阅读,如果你觉得有用,请分享给大家 :)

相关推荐
iFlyCai16 小时前
Xcode 16 pod init失败的解决方案
ios·xcode·swift
Hamm1 天前
先别急着喷,没好用的iOS-Ollama客户端那就自己写个然后开源吧
人工智能·llm·swift
CocoaKier1 天前
苹果商店下载链接如何获取
ios·apple
hxx2212 天前
iOS swift开发--- 加载PDF文件并显示内容
ios·pdf·swift
今天也想MK代码3 天前
基于ModelScope打造本地AI模型加速下载方案
ai·语言模型·swift·model·language model
袁代码3 天前
Swift 开发教程系列 - 第11章:内存管理和 ARC(Automatic Reference Counting)
开发语言·ios·swift·ios开发
一丝晨光4 天前
GCC和clang的爱恨情仇
macos·objective-c·xcode·apple·clang·gcc·llvm
袁代码4 天前
Swift 开发教程系列 - 第8章:协议与扩展
开发语言·ios·swift·ios开发
袁代码4 天前
Swift 开发教程系列 - 第9章:错误处理
开发语言·ios·swift·ios开发
iFlyCai4 天前
Swift中的Combine
开发语言·ios·swift·combine·swift combine