C#中实现值相等(Value Equality)的详细步骤

 更新时间:2026年01月12日 08:59:53   作者:无风听海  
在 C# 中,相等并不是一个简单的问题,很多开发者认为重写 Equals 就够了,但在真实系统中,错误或不完整的相等实现会导致一些bug,本文将从底层机制出发,给出标准、完整、可复用的值相等实现步骤,需要的朋友可以参考下

一、为什么“值相等”是一个需要认真对待的问题

在 C# 中,相等并不是一个简单的问题
很多开发者认为重写 Equals 就够了,但在真实系统中,错误或不完整的相等实现会导致:

  • Dictionary / HashSet 行为异常
  • 对象“看起来相等”,但集合中却当作不相等
  • ==EqualsContains 行为不一致
  • 隐蔽而难以排查的 Bug

这背后的原因在于:

.NET 的相等语义是一个由多个方法和接口共同构成的协作体系,而不是单一方法。

本文将从底层机制出发,给出标准、完整、可复用的实现步骤

二、相等的两种语义:引用相等 vs 值相等

在 .NET 中,存在两种本质不同的“相等”:

1. 引用相等(Reference Equality)

object.ReferenceEquals(a, b)
  • 判断两个变量是否指向同一对象实例
  • 类(reference type)的默认行为

2. 值相等(Value Equality)

  • 判断两个对象的数据内容是否相同
  • 由开发者显式定义和实现
var a = new Person("Tom", 18);
var b = new Person("Tom", 18);

ReferenceEquals(a, b); // false
a.Equals(b);           // true(如果实现了值相等)

三、.NET 相等体系的整体结构

实现值相等,必须理解以下四个关键成员的职责分工:

成员角色
IEquatable<T>.Equals(T)类型安全、性能最优的相等判断
object.Equals(object)所有 .NET API 的统一入口
GetHashCode()哈希集合的基础
== / != 运算符语法层面的相等判断(可选)

一个正确的值相等实现,必须保证这些成员在语义上一致。

四、类(引用类型)实现值相等的标准步骤

以下步骤适用于绝大多数引用类型(class)

Step 1:明确“相等”的语义(设计阶段)

首先必须回答一个设计问题:

哪些字段决定两个对象在业务语义上是“相等”的?

例如:

Person 相等 ⇔ Name 和 Age 都相等

这一步没有代码,但至关重要。

Step 2:实现IEquatable<T>.Equals(T other)(核心步骤)

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;
    }
}

为什么这是核心?

  • 泛型集合(HashSet<T>Dictionary<TKey, TValue>优先调用它
  • 避免装箱(boxing),性能优于 object.Equals
  • 提供类型安全的比较语义

IEquatable<T> 是值相等的主入口。

Step 3:重写object.Equals(object obj)(必须)

public override bool Equals(object obj)
{
    return Equals(obj as Person);
}

为什么必须?

  • 大量 .NET API 只接受 object
  • object.Equals(a, b)、非泛型集合依赖它
  • 保证所有调用路径的相等逻辑一致

规范要求

Equals(object) 必须委托给 Equals(T),而不是重复实现逻辑。

Step 4:重写GetHashCode()(必须)

public override int GetHashCode()
{
    return HashCode.Combine(Name, Age);
}

必须遵守的核心约束

如果 a.Equals(b) 为 true,
那么 a.GetHashCode() 必须等于 b.GetHashCode()。

否则:

  • HashSet<T> 会包含重复元素
  • Dictionary<TKey, TValue> 无法正确查找 key

实践建议

  • 使用参与相等比较的字段
  • 避免使用可变字段
  • 不要依赖 string.GetHashCode() 的持久性

Step 5(可选但推荐):重载== / !=运算符

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);
}

说明

  • 默认情况下,类的 == 比较的是引用
  • 重载后可使 == 与值相等语义一致
  • object.Equals 已处理所有 null 情况,是最安全的写法

五、完整标准实现模板

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);
}

六、结构体(值类型)的补充说明

  • struct 默认按字段比较,但使用反射,性能较低
  • 推荐同样实现 IEquatable<T>
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);
}

七、record:值相等的语言级支持(C# 9+)

public record Person(string Name, int Age);

编译器自动生成:

  • IEquatable<T>
  • Equals(object)
  • GetHashCode
  • == / !=
  • 不可变设计

对于值对象(Value Object),record 是首选方案。

八、常见错误总结

  • 只实现 IEquatable<T>,不重写 Equals(object)
  • 重写 Equals,但忘记 GetHashCode
  • ==Equals 语义不一致
  • GetHashCode 中使用可变字段
  • == 中直接调用 left.Equals(right)

九、结论

在 C# 中,实现值相等不是“重写一个方法”,
而是一个需要遵循明确步骤和约束的完整设计。

标准流程可以总结为:

  1. 明确定义相等语义
  2. 实现 IEquatable<T>.Equals(T)
  3. 重写 object.Equals(object)
  4. 重写 GetHashCode()
  5. (可选)重载 == / !=

这套实现,才能在语言层、集合层和框架层都保持一致、可靠的行为。

以上就是C#中实现值相等(Value Equality)的详细步骤的详细内容,更多关于C#实现值相等Value Equality的资料请关注脚本之家其它相关文章!

相关文章

  • C#正则表达式转义字符介绍

    C#正则表达式转义字符介绍

    正则表达式,又称正规表示法、常规表示法。这篇文章主要介绍了C#正则表达式转义字符介绍的相关资料,非常不错,具有参考借鉴价值,需要的朋友可以参考下
    2016-07-07
  • c# 异步编程入门

    c# 异步编程入门

    这篇文章主要介绍了c# 异步编程入门的相关资料,帮助大家更好的理解和学习使用c#,感兴趣的朋友可以了解下
    2021-03-03
  • C#统计字符串的方法

    C#统计字符串的方法

    这篇文章主要为大家详细介绍了C#统计字符串的方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-03-03
  • VS2010写的程序在自己电脑可以运行、其他电脑上不能运行的解决方案

    VS2010写的程序在自己电脑可以运行、其他电脑上不能运行的解决方案

    自己用Visual Studio 2010 旗舰版写了一个软件,在自己电脑上运行完全没有问题,但是拷贝到其他人电脑上之后不管双击还是以管理身份运行,均没有反应,进程管理器中相关进程也只是一闪而过
    2013-04-04
  • C#内存管理CLR深入讲解(下篇)

    C#内存管理CLR深入讲解(下篇)

    本文详细讲解了C#内存管理CLR的内存分配和对大对象回收,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-01-01
  • C#中using的使用方式详解

    C#中using的使用方式详解

    这篇文章主要介绍了C#中using的使用方式方式详解,本文通过示例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-06-06
  • C#中分部类和分部方法的应用

    C#中分部类和分部方法的应用

    本篇文章介绍了,C#中分部类和分部方法的应用。需要的朋友参考下
    2013-04-04
  • C#程序打成 一键安装包-InstallShield教程

    C#程序打成 一键安装包-InstallShield教程

    最近找到个打包工具,挺不错的。下面简单的介绍下使用方法
    2012-01-01
  • C#如何读写应用程序配置文件App.exe.config,并在界面上显示

    C#如何读写应用程序配置文件App.exe.config,并在界面上显示

    这篇文章主要介绍了C#如何读写应用程序配置文件App.exe.config,并在界面上显示问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2023-06-06
  • C#实现简单的字符串加密

    C#实现简单的字符串加密

    这篇文章介绍了C#实现字符串加密的方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-06-06

最新评论