首页 / Swift 编程语言教程 / Equatable/Comparable/Hashable

Swift 编程语言教程

Equatable/Comparable/Hashable

本教程共 93 篇 · 第 79 篇 · 更新于 2026-08-08 · 约 8 分钟阅读

SwiftEquatableComparableHashable协议相等哈希排序

本节目标:让自定义类型能判断相等、能排序、能当字典键或放进集合,并搞懂 Swift 怎么自动帮你合成这些能力。

你的自定义类型,比如一个 User、一个 Vector3D,能不能直接用 == 比较两个用户是否相等?能不能用 < 排序?能不能放进 Set 或当 Dictionary 的键?这些都需要它遵循三个基础协议:EquatableComparableHashable。好消息是,Swift 在大多数简单情况下能自动合成实现,你几乎不用写代码。

1-1 Equatable:判断”相等”

Equatable 协议让你自定义什么叫”两个实例相等”,于是可以用 ==!= 比较。对你来说,相等往往指”关键属性值一样”,而不是”是不是内存里同一个对象”。

最省事的情况:只要你的类型所有存储属性本身都遵循 Equatable,Swift 就自动合成 == 实现。你只需要在声明里写个遵循,一行都不用写:

struct Vector3D: Equatable {
    var x = 0.0, y = 0.0, z = 0.0
}

let a = Vector3D(x: 1, y: 2, z: 3)
let b = Vector3D(x: 1, y: 2, z: 3)
let c = Vector3D(x: 0, y: 0, z: 0)

print(a == b)   // true,三个分量都相等
print(a == c)   // false

能自动合成 Equatable 的类型包括:所有存储属性都遵循 Equatable 的结构体;没有关联值、或关联值都遵循 Equatable 的枚举。只要满足,写个冒号遵循即可。

Note

注意区分 ==(值相等,由 Equatable 定义)和 ===(引用同一实例,只类有)。两个不同类的实例即便属性全一样,=== 也返回 false,因为它们是两块内存。

1-2 手动实现 Equatable

有时候”相等”的定义不按全部属性来。比如比较两个用户只看身份证号,不看昵称。这时就自己实现 ==

struct User: Equatable {
    var id: Int
    var nickname: String

    static func == (lhs: User, rhs: User) -> Bool {
        return lhs.id == rhs.id   // 只看 id,忽略昵称
    }
}

let u1 = User(id: 1, nickname: "小明")
let u2 = User(id: 1, nickname: "明明")
print(u1 == u2)   // true,因为 id 相同

一旦你自己写了 ==,Swift 就不会再自动合成(也无需合成),!= 仍由协议默认提供。这种手动控制让你能精确定义”业务上相等”的含义。

1-3 Comparable:支持排序

Comparable 继承自 Equatable,让你能用 <<=>>= 比较,并能把一堆实例排序。遵循它时,你只需实现 <,其余比较运算符由标准库基于它推导。

对枚举尤其好用。Swift 会按声明顺序自动合成 <,先声明的更小:

enum SkillLevel: Comparable {
    case beginner
    case intermediate
    case expert
}

print(SkillLevel.beginner < SkillLevel.expert)   // true

带关联值的枚举也能自动合成,只要关联值类型可比较:

enum Medal: Comparable {
    case none
    case bronze
    case silver
    case gold
}

let results = [Medal.bronze, Medal.gold, Medal.none, Medal.silver]
let sorted = results.sorted()
print(sorted)   // [none, bronze, silver, gold]

对结构体,若想按某个字段排序,就手动实现 <

struct Student: Comparable {
    var name: String
    var score: Int

    static func < (lhs: Student, rhs: Student) -> Bool {
        return lhs.score < rhs.score   // 按分数升序
    }
}

let list = [Student(name: "甲", score: 80), Student(name: "乙", score: 95)]
print(list.sorted().map { $0.name })   // ["甲", "乙"]
Tip

排序类需求(排行榜、时间先后、优先级)几乎都靠 Comparable + sorted() 搞定。先遵守协议、实现 <,剩下的交给标准库。

1-4 Hashable:当键与去重

Hashable 让你把实例放进 Set,或当作 Dictionary 的键。集合和字典靠”哈希值”快速定位元素、判断重复,所以键类型必须可哈希。

和前两个一样,若所有属性都可哈希,Swift 自动合成 hash(into:)

struct Book: Hashable {
    var isbn: String
    var title: String
}

var library: Set<Book> = []
let book = Book(isbn: "978-1", title: "Swift 入门")
library.insert(book)
print(library.contains(book))   // true

字典同理:

var cache: [Book: String] = [:]
cache[book] = "已借阅"
print(cache[book] ?? "无记录")   // 已借阅

自动合成的条件是:所有存储属性都遵循 Hashable 的结构体;关联值都可哈希、或没有关联值的枚举。

Warning

哈希相等是”相等”的更强约束:如果两个实例 ==true,它们必须产生相同的哈希值;反之不成立(不同对象可能哈希冲突,但概率极低)。所以手动实现 Hashable 时,必须用和 == 完全一致的属性集合来算哈希,否则集合和字典会出诡异 bug。

1-5 三个协议的关系与自动合成小结

这三者经常一起出现,而且都能自动合成,记住下面这张表就够了:

协议用途需实现自动合成条件
Equatable== / !=自己写 == 或靠合成属性全 Equatable 的结构体;关联值全 Equatable 的枚举
Comparable排序比较自己写 < 或靠合成枚举按声明顺序;部分场景需手动 <
HashableSet/字典键靠合成或自定义 hash(into:)属性全 Hashable 的结构体/枚举

Comparable 隐含 EquatableHashable 也常与 Equatable 一起声明。一个类型常常三者全遵循:

struct Product: Equatable, Comparable, Hashable {
    var sku: String
    var price: Int
    static func < (lhs: Product, rhs: Product) -> Bool {
        return lhs.price < rhs.price
    }
}
Note

合成实现有个前提:遵循声明必须写在原始类型定义所在的同一个文件里,不能隔文件。这是 Swift 合成机制的限制,写扩展时别跨文件。

1-6 常见坑

第一,混用手动 == 和自动 Hashable 时,哈希用的属性和你定义的”相等属性”不一致,会导致 Set 里出现你以为相等的重复元素。务必让两者基于同一组属性。

第二,类要遵循这些协议时,默认不会自动合成,因为类有继承,编译器不知道哪些属性该参与比较或哈希。类需要你手动实现,或者改用结构体。

第三,浮点数 Double 参与 == 要小心,因为精度误差可能让”理论上相等”的两个数被判为不等。涉及浮点相等最好定义容差。

1-7 把三者合起来用

真实业务里这三个协议常常同时出现。比如一个表示”待办事项”的模型,你想按创建时间排序、想用 Set 去重、想当字典键按内容查:

struct Todo: Equatable, Comparable, Hashable {
    var id: UUID
    var title: String
    var createdAt: Date

    static func < (lhs: Todo, rhs: Todo) -> Bool {
        return lhs.createdAt < rhs.createdAt   // 按时间升序
    }
}

这里 Comparable 你手动实现了 <EquatableHashable 则因为 idtitlecreatedAt 都可哈希、可相等,Swift 自动合成。于是这个类型既能 sorted(),又能进 Set、当字典键,一行额外代码都不用为相等和哈希写。

但要注意一个隐含约束:自动合成的 ==hashValue 都会用全部存储属性。如果你在 Comparable 里只按 createdAt 排序,却在业务上认为”id 相同即同一事项”,那相等判断和排序的语义就割裂了。这种不一致会让 Set 里出现你以为相同、实则因 title 不同而被视为不同的元素。

Tip

当相等、哈希、排序三者语义可能不一致时,主动手动实现三者,让它们基于同一组”业务主键”(通常是 id),避免自动合成各行其是。

1-8 小结

EquatableComparableHashable 是 Swift 标准库的基石协议,分别支撑相等判断、排序、Set/字典键。三者都满足”属性全可遵循则自动合成”的甜头,让你几乎零代码获得这些能力;需要特殊语义时再手动实现对应方法。

至此,模块九”内存管理与值语义”全部讲完:你从 ARC 一路走到值语义、COW,再到如何让类型可比较可哈希。下一章起进入模块十”并发编程”,那是 Swift 现代语法的另一座大山。