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

编写高效且易于测验的代码,削减过错的产生。

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

什么是类型?

这个问题的答案适当简单——**类型是一个相关数据的调集,封装了某些概念和原则。**一个类型能够是抽象的,也能够是具体的,它界说了一组咱们能够对其进行的有效操作,最后,一个类型明确划分了内部和外部的边界(封装性的体现)。

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

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

func getAddress() -> String
func getAddress() -> Any

以上所给的函数中,哪个更简单测验?第二个函数提供了更多的灵活性,由于它答应咱们回来 Any 类型,可是在测验办法的正确性时,相同的灵活性被证明是具有挑战性的。假如咱们看一下第一个函数,咱们能够确认它将回来一个String类型。

仅仅通过调查第一个函数,咱们就能够看到它每次都会回来一个String,可是第二个函数就不一样了,这就使得它更难测验。

Equality 和 Identity / 等同性和仅有性

咱们先凭借下面的比如来探讨 “Equality”:

struct EmployeeID {
  private(set) var id: Int
    init?(_ raw: Int) {
        guard raw > 1000 else {
            return nil
        }
        id = raw
    }
}

咱们有一个 EmployeeID 类型。

咱们有一个 EmployeeID 的可失败初始化器,能够避免创立一个小于 4 位数的 EmployeeID

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

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 类型。编译器隐含地生成了一致性要求,因而,履行上面的代码会在控制台中打印以下内容。

Swift 类型中的 Equality, Identity 和 Hashing

让咱们凭借于下面的比如来研究引证类型的 Equality 行为

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,目前也有这个问题,假如有一个循环,它将创立一个无限的循环。

解决上述问题的办法非常简单——只需在代码中完成 “==“,如下所示:

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

标准的完成是将左面的一切特点与右边的进行比较。

引证类型除了 Equality 之外还有 Identity 的概念,让咱们凭借下面的代码比如看看这两个概念有什么不同:

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 目标,看它们是否持平。咱们还用 “==” 和 “===” 运算符来查看它们是否相同。履行上述代码的结果是在控制台中打印出以下内容:

Swift 类型中的 Equality, Identity 和 Hashing

===” 查看两个目标是否指向相同的引证,由于两个引证都指向内存中持平但不同的方位,比较这些目标身份的结果是过错的,即 firstEmployeecopyOfFirstEmployee 是持平的,但它们在内存中并不指向同一个目标。

假如咱们把 firstEmployee 分配给一个新的变量,然后查看这两个变量的 Identity,会产生什么?

let refereceToFirstEmployee = firstEmployee
print("Both References are (refereceToFirstEmployee === firstEmployee ? "Identical" : "Unidentical")")

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

Swift 类型中的 Equality, Identity 和 Hashing

证明这一次,两个目标现在指的是内存中的同一个方位。

咱们能够灵活地界说 “==“,但最好是保持简单,比较一切的特点,只要咱们不产生无限循环。

添加 Equatable 一致性使模型更可用,更简单测验。

Hashable

Hashable 是一种比较目标的更强大的办法。当咱们需求在 DictionarySet 中进行快速查找时,它很有用。

Set 中的元素或 Dictionary 中的键有必要是可哈希的;不然,编译器会开端诉苦。

别的,Hashable 也需求 Equatable 一致性。让咱们看看它是如何作业的。假定咱们有一个包含 7 个可哈希目标的 Set,如下图所示:

Swift 类型中的 Equality, Identity 和 Hashing

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 变得非常直观和清晰。让咱们来看看这段代码:

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

谢谢你的阅览,假如你觉得有用,请共享给大家 :)