C#中实现值相等(Value Equality)的详细步骤
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>一、为什么“值相等”是一个需要认真对待的问题</li><li>二、相等的两种语义:引用相等 vs 值相等</li><ul class="second_class_ul"><li>1. 引用相等(Reference Equality)</li><li>2. 值相等(Value Equality)</li></ul><li>三、.NET 相等体系的整体结构</li><ul class="second_class_ul"></ul><li>四、类(引用类型)实现值相等的标准步骤</li><ul class="second_class_ul"><li>Step 1:明确“相等”的语义(设计阶段)</li><li>Step 2:实现IEquatable<T>.Equals(T other)(核心步骤)</li><ul class="third_class_ul"><li>为什么这是核心?</li></ul><li>Step 3:重写object.Equals(object obj)(必须)</li><ul class="third_class_ul"><li>为什么必须?</li></ul><li>Step 4:重写GetHashCode()(必须)</li><ul class="third_class_ul"><li>必须遵守的核心约束</li><li>实践建议</li></ul><li>Step 5(可选但推荐):重载== / !=运算符</li><ul class="third_class_ul"><li>说明</li></ul></ul><li>五、完整标准实现模板</li><ul class="second_class_ul"></ul><li>六、结构体(值类型)的补充说明</li><ul class="second_class_ul"></ul><li>七、record:值相等的语言级支持(C# 9+)</li><ul class="second_class_ul"></ul><li>八、常见错误总结</li><ul class="second_class_ul"></ul><li>九、结论</li><ul class="second_class_ul"></ul></ul></div><p class="maodian"></p><h2>一、为什么“值相等”是一个需要认真对待的问题</h2><p>在 C# 中,<strong>相等并不是一个简单的问题</strong>。<br />很多开发者认为重写 <code>Equals</code> 就够了,但在真实系统中,错误或不完整的相等实现会导致:</p>
<ul><li><code>Dictionary</code> / <code>HashSet</code> 行为异常</li><li>对象“看起来相等”,但集合中却当作不相等</li><li><code>==</code>、<code>Equals</code>、<code>Contains</code> 行为不一致</li><li>隐蔽而难以排查的 Bug</li></ul>
<p>这背后的原因在于:</p>
<blockquote><p><strong>.NET 的相等语义是一个由多个方法和接口共同构成的协作体系,而不是单一方法。</strong></p></blockquote>
<p>本文将从底层机制出发,给出<strong>标准、完整、可复用的实现步骤</strong>。</p>
<p class="maodian"></p><h2>二、相等的两种语义:引用相等 vs 值相等</h2>
<p>在 .NET 中,存在两种本质不同的“相等”:</p>
<p class="maodian"></p><h3>1. 引用相等(Reference Equality)</h3>
<div class="jb51code"><pre class="brush:csharp;">object.ReferenceEquals(a, b)
</pre></div>
<ul><li>判断两个变量是否指向<strong>同一对象实例</strong></li><li>类(reference type)的默认行为</li></ul>
<p class="maodian"></p><h3>2. 值相等(Value Equality)</h3>
<ul><li>判断两个对象的<strong>数据内容是否相同</strong></li><li>由开发者显式定义和实现</li></ul>
<div class="jb51code"><pre class="brush:csharp;">var a = new Person("Tom", 18);
var b = new Person("Tom", 18);
ReferenceEquals(a, b); // false
a.Equals(b); // true(如果实现了值相等)
</pre></div>
<p class="maodian"></p><h2>三、.NET 相等体系的整体结构</h2>
<p>实现值相等,必须理解以下四个关键成员的职责分工:</p>
<table><thead><tr><th>成员</th><th>角色</th></tr></thead><tbody><tr><td><code>IEquatable<T>.Equals(T)</code></td><td>类型安全、性能最优的相等判断</td></tr><tr><td><code>object.Equals(object)</code></td><td>所有 .NET API 的统一入口</td></tr><tr><td><code>GetHashCode()</code></td><td>哈希集合的基础</td></tr><tr><td><code>== / !=</code> 运算符</td><td>语法层面的相等判断(可选)</td></tr></tbody></table>
<p><strong>一个正确的值相等实现,必须保证这些成员在语义上一致。</strong></p>
<p class="maodian"></p><h2>四、类(引用类型)实现值相等的标准步骤</h2>
<p>以下步骤适用于<strong>绝大多数引用类型(class)</strong>。</p>
<p class="maodian"></p><h3>Step 1:明确“相等”的语义(设计阶段)</h3>
<p>首先必须回答一个设计问题:</p>
<blockquote><p><strong>哪些字段决定两个对象在业务语义上是“相等”的?</strong></p></blockquote>
<p>例如:</p>
<div class="jb51code"><pre class="brush:csharp;">Person 相等 ⇔ Name 和 Age 都相等
</pre></div>
<p>这一步没有代码,但至关重要。</p>
<p class="maodian"></p><h3>Step 2:实现IEquatable<T>.Equals(T other)(核心步骤)</h3>
<div class="jb51code"><pre class="brush:csharp;">public sealed class Person : IEquatable<Person>
{
public string Name { get; }
public int Age { get; }
public bool Equals(Person other)
{
if (other is null) return false;
return Name == other.Name && Age == other.Age;
}
}
</pre></div>
<p class="maodian"></p><h4>为什么这是核心?</h4>
<ul><li>泛型集合(<code>HashSet<T></code>、<code>Dictionary<TKey, TValue></code>)<strong>优先调用它</strong></li><li>避免装箱(boxing),性能优于 <code>object.Equals</code></li><li>提供类型安全的比较语义</li></ul>
<blockquote><p><strong>IEquatable<T> 是值相等的主入口。</strong></p></blockquote>
<p class="maodian"></p><h3>Step 3:重写object.Equals(object obj)(必须)</h3>
<div class="jb51code"><pre class="brush:csharp;">public override bool Equals(object obj)
{
return Equals(obj as Person);
}
</pre></div>
<p class="maodian"></p><h4>为什么必须?</h4>
<ul><li>大量 .NET API 只接受 <code>object</code></li><li><code>object.Equals(a, b)</code>、非泛型集合依赖它</li><li>保证所有调用路径的相等逻辑一致</li></ul>
<p><strong>规范要求</strong>:</p>
<blockquote><p>Equals(object) 必须委托给 Equals(T),而不是重复实现逻辑。</p></blockquote>
<p class="maodian"></p><h3>Step 4:重写GetHashCode()(必须)</h3>
<div class="jb51code"><pre class="brush:csharp;">public override int GetHashCode()
{
return HashCode.Combine(Name, Age);
}
</pre></div>
<p class="maodian"></p><h4>必须遵守的核心约束</h4>
<blockquote><p><strong>如果 a.Equals(b) 为 true,<br />那么 a.GetHashCode() 必须等于 b.GetHashCode()。</strong></p></blockquote>
<p>否则:</p>
<ul><li><code>HashSet<T></code> 会包含重复元素</li><li><code>Dictionary<TKey, TValue></code> 无法正确查找 key</li></ul>
<p class="maodian"></p><h4>实践建议</h4>
<ul><li>使用参与相等比较的字段</li><li>避免使用可变字段</li><li>不要依赖 <code>string.GetHashCode()</code> 的持久性</li></ul>
<p class="maodian"></p><h3>Step 5(可选但推荐):重载== / !=运算符</h3>
<div class="jb51code"><pre class="brush:csharp;">public static bool operator ==(Person left, Person right)
{
return object.Equals(left, right);
}
public static bool operator !=(Person left, Person right)
{
return !object.Equals(left, right);
}
</pre></div>
<p class="maodian"></p><h4>说明</h4>
<ul><li>默认情况下,类的 <code>==</code> 比较的是引用</li><li>重载后可使 <code>==</code> 与值相等语义一致</li><li><code>object.Equals</code> 已处理所有 <code>null</code> 情况,是最安全的写法</li></ul>
<p class="maodian"></p><h2>五、完整标准实现模板</h2>
<div class="jb51code"><pre class="brush:csharp;">public sealed class Person : IEquatable<Person>
{
public string Name { get; }
public int Age { get; }
public Person(string name, int age)
{
Name = name;
Age = age;
}
public bool Equals(Person other)
{
if (other is null) return false;
return Name == other.Name && Age == other.Age;
}
public override bool Equals(object obj)
=> Equals(obj as Person);
public override int GetHashCode()
=> HashCode.Combine(Name, Age);
public static bool operator ==(Person left, Person right)
=> object.Equals(left, right);
public static bool operator !=(Person left, Person right)
=> !object.Equals(left, right);
}
</pre></div>
<p class="maodian"></p><h2>六、结构体(值类型)的补充说明</h2>
<ul><li><code>struct</code> 默认按字段比较,但使用反射,性能较低</li><li>推荐同样实现 <code>IEquatable<T></code>:</li></ul>
<div class="jb51code"><pre class="brush:csharp;">public struct Point : IEquatable<Point>
{
public int X;
public int Y;
public bool Equals(Point other)
=> X == other.X && Y == other.Y;
public override bool Equals(object obj)
=> obj is Point p && Equals(p);
public override int GetHashCode()
=> HashCode.Combine(X, Y);
}
</pre></div>
<p class="maodian"></p><h2>七、record:值相等的语言级支持(C# 9+)</h2>
<div class="jb51code"><pre class="brush:csharp;">public record Person(string Name, int Age);
</pre></div>
<p>编译器自动生成:</p>
<ul><li><code>IEquatable<T></code></li><li><code>Equals(object)</code></li><li><code>GetHashCode</code></li><li><code>== / !=</code></li><li>不可变设计</li></ul>
<p><strong>对于值对象(Value Object),record 是首选方案。</strong></p>
<p class="maodian"></p><h2>八、常见错误总结</h2>
<ul><li>只实现 <code>IEquatable<T></code>,不重写 <code>Equals(object)</code></li><li>重写 <code>Equals</code>,但忘记 <code>GetHashCode</code></li><li><code>==</code> 与 <code>Equals</code> 语义不一致</li><li>在 <code>GetHashCode</code> 中使用可变字段</li><li>在 <code>==</code> 中直接调用 <code>left.Equals(right)</code></li></ul>
<p class="maodian"></p><h2>九、结论</h2>
<blockquote><p><strong>在 C# 中,实现值相等不是“重写一个方法”,<br />而是一个需要遵循明确步骤和约束的完整设计。</strong></p></blockquote>
<p>标准流程可以总结为:</p>
<ol><li>明确定义相等语义</li><li>实现 <code>IEquatable<T>.Equals(T)</code></li><li>重写 <code>object.Equals(object)</code></li><li>重写 <code>GetHashCode()</code></li><li>(可选)重载 <code>== / !=</code></li></ol>
<p>这套实现,才能在语言层、集合层和框架层都保持一致、可靠的行为。</p>
<p>以上就是C#中实现值相等(Value Equality)的详细步骤的详细内容,更多关于C#实现值相等Value Equality的资料请关注琼殿技术社区其它相关文章!</p>
<div class="art_xg">
<b>您可能感兴趣的文章:</b><ul><li>C# 如何实现一个基于值相等性比较的字典</li><li>C# 空值处理运算符??、?. 及其它常用符号</li><li>C#中字符串插值($) 和 逐字字符串(@)的使用</li><li>C#窗体间传值的两种实现方法</li></ul>
</div>
</div>
<!--endmain-->
頁:
[1]