为什么要"自定义"Hashable?
官方文档只告诉你"Set/Dictionary 的元素/键必须 Hashable",却没说:
- 系统默认的哈希算法什么时候会"翻车"?
 - 手写 
hash(into:)怎样既保证"分布均匀"又保证"向后兼容"? - struct 与 class 在"值语义"下对哈希的影响到底有何不同?
 
下面用 3 个真实踩坑案例回答。
案例 1:struct 默认 Hashable 够用吗?
            
            
              swift
              
              
            
          
          struct User: Hashable {
    let userId: Int
    let name: String
}
        上面代码完全合法,也能直接丢进 Set。
但产品迭代后,需求改成"同一 userId 即算同一人,name 可改"。
此时默认生成的 == 与 hash(into:) 仍然把 name 算进去,导致:
            
            
              swift
              
              
            
          
          let u1 = User(userId: 1, name: "A")
let u2 = User(userId: 1, name: "B")
print(Set([u1, u2]).count)   // 输出 2,业务想要 1
        解决:只拿"业务主键"做哈希。
            
            
              swift
              
              
            
          
          extension User {
    // 1)重载 ==,只比 userId
    static func == (lhs: User, rhs: User) -> Bool {
        lhs.userId == rhs.userId
    }
    // 2)手写 hash(into:),只混入 userId
    func hash(into hasher: inout Hasher) {
        hasher.combine(userId)
    }
}
        结论:
- 默认合成只适用于"所有存储属性都是关键"的场景;
 - 一旦业务主键≠全部属性,必须手写,避免"同名不同人"或"同人不同名"逻辑错误。
 
案例 2:class 的"对象身份"陷阱
            
            
              swift
              
              
            
          
          class Photo: Hashable {
    var url: String
    init(url: String) { self.url = url }
    
    static func == (lhs: Photo, rhs: Photo) -> Bool {
        lhs.url == rhs.url          // 只比内容
    }
    func hash(into hasher: inout Hasher) {
        hasher.combine(url)
    }
}
        看起来没问题,但 class 默认是"引用类型"。
            
            
              swift
              
              
            
          
          let p1 = Photo(url: "cat.jpg")
let p2 = p1                       // 只复制指针
p1.url = "dog.jpg"
print(p2.url)                     // dog.jpg,意外共享
        把实例扔进 Dictionary 后,外部改属性 → 哈希值变 → 字典 桶索引失效 → 找不到 key,这不是崩溃,是静默逻辑 bug。
方案:
- 用 struct 就能天然隔离;
 - 如果必须用 class(例如 @objc 继承),加 
final并把属性设为let,或手动保证"哈希依赖字段"不可变。 
案例 3:哈希碰撞与性能
写个"极差"的哈希函数:
            
            
              swift
              
              
            
          
          struct BadKey: Hashable {
    let value: Int
    func hash(into hasher: inout Hasher) {
        hasher.combine(0)   // 所有实例哈希值相同
    }
    static func == (lhs: BadKey, rhs: BadKey) -> Bool {
        lhs.value == rhs.value
    }
}
        测试:
            
            
              swift
              
              
            
          
          let N = 1_0000
let dict = Dictionary(uniqueKeysWithValues: (0..<N).map { (BadKey(value: $0), $0) })
// 查找耗时:O(N) 退化到链表
        Playground 时间线:
- 正常哈希:查找 1 万次 ≈ 0.2 ms
 - BadKey:查找 1 万次 ≈ 25 ms,125 倍差距
 
结论:
- 哈希不要求"密码安全",但分布必须均匀;
 - 多字段场景,用 
Hasher连续combine即可,别偷懒写xor大法。 
值语义 & 写时复制(COW)
- Array/Set/Dictionary 都是"值语义",但内部通过引用 + 写时复制优化。
 - 何时触发复制?
- 对变量做任何"可能改变其内容"的操作时,且引用计数 > 1。
 
 - 大集合性能陷阱:
 
            
            
              swift
              
              
            
          
          var a = Array(0..<1_000_000)
var b = a          // O(1),只复制指针
b[0] = -1          // 这一刻才复制 1_000_000 个元素
        - 提前规避:
 
            
            
              swift
              
              
            
          
          a.reserveCapacity(a.count + 100)   // 减少后续再分配
        - 对 Dictionary/Set 同样适用:
 
            
            
              swift
              
              
            
          
          dict.reserveCapacity(expectedCount)
        容量预分配实战
场景:批量解析 JSON,逐步往数组里 append。
            
            
              swift
              
              
            
          
          // 普通写法:可能多次重新分配
var output: [Item] = []
for json in jsonArray {
    output.append(parse(json))
}
// 优化:一次到位
var output: [Item] = []
output.reserveCapacity(jsonArray.count)
for json in jsonArray {
    output.append(parse(json))
}
        Benchmark(100 万元素):
- 无 reserve:总耗时 380 ms,分配 18 次
 - 有 reserve:总耗时 280 ms,分配 1 次
 
节省约 26% ,且代码只多一行。
在集合里存 @objc / NSError?
Objective-C 桥接类型(如 NSString、NSArray、NSError)并非真正的值类型,桥接到 Swift 集合后:
- 会触发"隐式转换",可能额外分配;
 - 作为 Dictionary Key 时,哈希实现走 NSObject 的 
hash属性,与 Swift 哈希算法不一致,跨语言传递时可能出意外。 
建议:
- 纯 Swift 模块尽量用 Swift 原生类型;
 - 必须桥接时,用 
as String/as NSError` 明确转换,再入库。 
弱引用键 & 自动清理
Swift 标准库目前没有 NSMapTable 的"弱引用键"版本。
若要做"缓存 + 自动释放":
- 用 NSMapTable 桥接(失去值语义);
 - 或用第三方库 WeakMap ;
 - 也可自己包一层:
 
            
            
              swift
              
              
            
          
          final class WeakBox<T: AnyObject> {
    weak var value: T?
    init(_ value: T) { self.value = value }
}
struct WeakDictionary<Key: Hashable, Value: AnyObject> {
    private var storage: [Key: WeakBox<Value>] = [:]
    mutating func cleanup() {
        storage = storage.filter { $0.value.value != nil }
    }
}
        注意:需手动或定时调用 cleanup(),否则容器会越来越大。
Swift 5.9 新玩意:@inlinable & 集合
给集合写 extension 时,如果加 @inlinable:
- 允许编译器跨模块内联,提高 Release 性能;
 - 但会暴露实现细节,library evolution 需打开 BUILD_LIBRARY_FOR_DISTRIBUTION。
 
结论:
- App 内模块可大胆用;
 - 开源库想保持 ABI 稳定,慎加。
 
性能清单速查表
| 操作 | Array | Set | Dictionary | 
|---|---|---|---|
| 随机访问 | O(1) | 不支持 | O(1) 平均 | 
| 末尾追加 | O(1) 摊销 | O(1) 平均 | --- | 
| 查找 | O(n) | O(1) 平均 | O(1) 平均 | 
| 插入/删除中间 | O(n) | O(1) 平均 | O(1) 平均 | 
| 顺序遍历 | 最快 | 慢(需跳转桶) | 慢 | 
| 去重 | 需转 Set | 天生 | --- | 
总结
- 默认合成的 Hashable 经常≠业务主键,手写时只哈希"不变的主键"。
 - class 的引用身份会破坏值语义,哈希依赖字段必须不可变。
 - 哈希碰撞会让 Set/Dictionary 性能瞬间崩塌,多字段就用 
hasher.combine连续混写。 - 大集合提前 
reserveCapacity,写时复制能省 2030% 时间。 - 与 Objective-C 桥接时,注意 NSString/NSError 的哈希算法差异。