Equatable/Comparable/Hashable
本教程共 93 篇 · 第 79 篇 · 更新于 2026-08-08 · 约 8 分钟阅读
本节目标:让自定义类型能判断相等、能排序、能当字典键或放进集合,并搞懂 Swift 怎么自动帮你合成这些能力。
你的自定义类型,比如一个 User、一个 Vector3D,能不能直接用 == 比较两个用户是否相等?能不能用 < 排序?能不能放进 Set 或当 Dictionary 的键?这些都需要它遵循三个基础协议:Equatable、Comparable、Hashable。好消息是,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 | 排序比较 | 自己写 < 或靠合成 | 枚举按声明顺序;部分场景需手动 < |
Hashable | Set/字典键 | 靠合成或自定义 hash(into:) | 属性全 Hashable 的结构体/枚举 |
Comparable 隐含 Equatable,Hashable 也常与 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 你手动实现了 <;Equatable 和 Hashable 则因为 id、title、createdAt 都可哈希、可相等,Swift 自动合成。于是这个类型既能 sorted(),又能进 Set、当字典键,一行额外代码都不用为相等和哈希写。
但要注意一个隐含约束:自动合成的 == 和 hashValue 都会用全部存储属性。如果你在 Comparable 里只按 createdAt 排序,却在业务上认为”id 相同即同一事项”,那相等判断和排序的语义就割裂了。这种不一致会让 Set 里出现你以为相同、实则因 title 不同而被视为不同的元素。
Tip当相等、哈希、排序三者语义可能不一致时,主动手动实现三者,让它们基于同一组”业务主键”(通常是
id),避免自动合成各行其是。
1-8 小结
Equatable、Comparable、Hashable 是 Swift 标准库的基石协议,分别支撑相等判断、排序、Set/字典键。三者都满足”属性全可遵循则自动合成”的甜头,让你几乎零代码获得这些能力;需要特殊语义时再手动实现对应方法。
至此,模块九”内存管理与值语义”全部讲完:你从 ARC 一路走到值语义、COW,再到如何让类型可比较可哈希。下一章起进入模块十”并发编程”,那是 Swift 现代语法的另一座大山。