引言:那个拖慢你代码的典型错误
\n\n每个至少写过一次 C# 并接触过 Dictionary 或 HashSet 的人,迟早都会在 Stack Overflow 上遇到这个问题。数千次浏览,数十个答案,但核心只有一个:如果你在自己的类中重写了 Equals,却忘记了 GetHashCode——你的代码可能运行不正确或极其缓慢。为什么?让我们通过实际示例来弄清楚。
想象一下,你正在编写一个机票预订系统。你有一个包含唯一航班号和日期的 Flight 类。你想将这个类用作 Dictionary 中的键。看起来一切都很简单。但如果没有正确的 GetHashCode,你的字典可能会退化成一个链表,搜索速度从 O(1) 变成 O(n)。
哈希表的魔力:Dictionary 在 C# 中如何工作
\n\n要理解这个问题,我们需要深入 Dictionary 的内部。它内部是一个“桶”(buckets)数组。当你添加一个元素时,C# 会调用键的 GetHashCode() 方法来确定桶的编号。如果桶中已有元素,则会调用 Equals() 与每个元素进行比较。正是这种协同作用实现了闪电般的快速查找。
不可违反的约定
\n在 C# 中,存在一个不成文但严格的约定:
\n- \n
- 如果两个对象的
Equals()返回true,那么它们的哈希码必须相等。 \n - 如果哈希码不同,则永远不会调用
Equals()(这是一种优化)。 \n - 反之则不一定:不同的对象可以有相同的哈希码(冲突)。 \n
当你只重写 Equals 而不重写 GetHashCode 时,你会继承自 System.Object 的实现,该实现基于内存中的引用。根据你的逻辑“相等”的两个对象将具有不同的哈希码。这就是噩梦的开始。
问题示例:在字典中丢失的航班
\n\n看看这段代码。它看起来应该能工作,但实际上却不能:
\n\npublic class Flight\n{\n public string FlightNumber { get; }\n public DateTime Date { get; }\n\n public Flight(string flightNumber, DateTime date)\n {\n FlightNumber = flightNumber;\n Date = date;\n }\n\n public override bool Equals(object obj)\n {\n if (obj is Flight other)\n return FlightNumber == other.FlightNumber && Date == other.Date;\n return false;\n }\n \n // GetHashCode 未重写!\n}\n\n// 使用\nvar flights = new Dictionary<Flight, string>();\nvar f1 = new Flight("SU100", new DateTime(2024, 12, 25));\nvar f2 = new Flight("SU100", new DateTime(2024, 12, 25));\n\nflights[f1] = "莫斯科 - 巴黎";\n\n// 错误!找不到 f2,尽管 f1.Equals(f2) == true\nConsole.WriteLine(flights.ContainsKey(f2)); // False\n\n为什么会这样?f1 和 f2 是内存中不同的对象。它们从 Object.GetHashCode() 获得不同的哈希码。字典将 f1 放入第 123 号桶,却在第 456 号桶中查找 f2。Equals 甚至没有被调用!结果就是找不到元素。
性能问题:当一切都能工作,但速度很慢
\n\n最糟糕的情况是部分解决方案。假设你重写了 GetHashCode,但做得很差,例如返回一个常量:
public override int GetHashCode() => 42; // 所有对象都在同一个桶中\n\n形式上,约定得到了遵守(相等的对象具有相同的哈希码)。但性能会崩溃:所有对象都会进入同一个桶,Dictionary 退化为一个链表。插入和查找将变成 O(n)。对于一个包含 10,000 个元素的字典来说,这是一场灾难。