IEqualityComparer

IEqualityComparer<T> 完全解析(LINQ/集合核心接口)

IEqualityComparer<T> 是 C# 中用于自定义对象相等性判断的核心接口,解决了“值类型按值比较、引用类型默认按引用比较”的局限性,广泛用于 LINQ(Union/Except/Distinct 等)、HashSet<T>Dictionary<TKey,TValue> 等场景。

一、核心作用

默认情况下:

  • 值类型(int/string/DateTime):按“值”比较相等性;
  • 引用类型(自定义类如 Student):按“内存地址(引用)”比较,即使两个对象字段完全相同,也会被判定为不相等。

IEqualityComparer<T> 的作用:自定义相等性规则(如按 Id/Name 等字段判断对象是否相等),让 LINQ/集合按业务规则处理数据。

二、接口定义与核心方法

IEqualityComparer<T> 位于 System.Collections.Generic 命名空间,包含两个必须实现的方法:

方法 作用
bool Equals(T x, T y) 定义“两个对象是否相等”的规则(如按 Id 相等则对象相等)
int GetHashCode(T obj) 生成对象的哈希码,必须与 Equals 逻辑一致(否则哈希表失效)

核心原则

  • Equals(x, y) 返回 true,则 xyGetHashCode 必须返回相同值;
  • Equals(x, y) 返回 falseGetHashCode 尽量返回不同值(提升哈希表性能);
  • 处理 null 场景(避免空引用异常)。

三、完整实现示例

1. 基础场景:按单个字段(Id)比较

Student 类为例,实现按 Id 判断相等:

using System;
using System.Collections.Generic;
using System.Linq;

// 自定义实体类
public class Student
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Score { get; set; }
}

// 自定义相等比较器:按Id比较Student
public class StudentIdComparer : IEqualityComparer<Student>
{
    // 步骤1:实现Equals方法(核心相等规则)
    public bool Equals(Student x, Student y)
    {
        // 1. 两个对象都为null → 相等
        if (x == null && y == null) return true;
        // 2. 其中一个为null → 不相等
        if (x == null || y == null) return false;
        // 3. 自定义规则:Id相等则对象相等
        return x.Id == y.Id;
    }

    // 步骤2:实现GetHashCode方法(与Equals逻辑一致)
    public int GetHashCode(Student obj)
    {
        // 处理null:返回0
        if (obj == null) return 0;
        // 基于Id生成哈希码(保证Id相同则哈希码相同)
        return obj.Id.GetHashCode();
    }
}

2. 进阶场景:按多个字段(Id+Name)比较

若需按「Id+Name」双重规则判断相等:

public class StudentMultiFieldComparer : IEqualityComparer<Student>
{
    public bool Equals(Student x, Student y)
    {
        if (x == null && y == null) return true;
        if (x == null || y == null) return false;
        // 多字段规则:Id和Name都相等才视为相等
        return x.Id == y.Id && x.Name == y.Name;
    }

    public int GetHashCode(Student obj)
    {
        if (obj == null) return 0;
        // 基于多个字段生成哈希码(HashCode.Combine自动处理多字段)
        return HashCode.Combine(obj.Id, obj.Name);
    }
}

四、核心使用场景

场景1:LINQ 方法(Union/Except/Distinct)

// 测试数据
List<Student> list1 = new()
{
    new Student { Id = 1, Name = "张三", Score = 85 },
    new Student { Id = 2, Name = "李四", Score = 92 }
};

List<Student> list2 = new()
{
    new Student { Id = 2, Name = "李四", Score = 92 }, // 与list1的李四重复(按Id)
    new Student { Id = 3, Name = "王五", Score = 78 }
};

// 1. Distinct:按Id去重
var distinctStudents = list1.Concat(list2)
                            .Distinct(new StudentIdComparer());
// 结果:Id=1、2、3的学生(去重了Id=2的重复项)

// 2. Union:合并去重(等价于Concat+Distinct)
var unionStudents = list1.Union(list2, new StudentIdComparer());

// 3. Except:筛选list1独有的学生
var exceptStudents = list1.Except(list2, new StudentIdComparer());
// 结果:仅Id=1的张三(Id=2的李四在list2中存在)

场景2:HashSet(自定义相等规则)

HashSet<T> 默认按引用比较,使用比较器后按自定义规则去重:

// 创建HashSet,指定自定义比较器
var studentSet = new HashSet<Student>(new StudentIdComparer());

// 添加元素(Id=2的李四会被判定为重复,无法添加)
studentSet.Add(new Student { Id = 2, Name = "李四" });
studentSet.Add(new Student { Id = 2, Name = "李四" });

Console.WriteLine(studentSet.Count); // 输出:1

场景3:Dictionary<TKey,TValue>(自定义键比较)

// 以Student为键(按Id比较),分数为值
var scoreDict = new Dictionary<Student, int>(new StudentIdComparer());

var stu1 = new Student { Id = 1, Name = "张三" };
var stu2 = new Student { Id = 1, Name = "张三" };

// stu1和stu2被判定为同一个键,赋值会覆盖
scoreDict[stu1] = 85;
scoreDict[stu2] = 90;

Console.WriteLine(scoreDict[stu1]); // 输出:90

五、常见坑与避坑指南

坑1:GetHashCodeEquals 逻辑不一致

// 错误示例:Equals按Id,GetHashCode按Name
public bool Equals(Student x, Student y) => x.Id == y.Id;
public int GetHashCode(Student obj) => obj.Name.GetHashCode();

// 后果:Id相同但Name不同的对象,Equals返回true,但哈希码不同
// LINQ/HashSet会判定为不相等,导致去重/比较失效

✅ 解决:哈希码必须基于 Equals 中用到的所有字段

坑2:未处理 null 场景

// 错误示例:未判断null,调用obj.Id会抛空引用异常
public bool Equals(Student x, Student y) => x.Id == y.Id;
public int GetHashCode(Student obj) => obj.Id.GetHashCode();

✅ 解决:先判断 x/y/obj 是否为 null

坑3:可变字段作为哈希依据

StudentId 是可修改的,修改后哈希码会变化,导致 HashSet/Dictionary 无法找到该元素:

var stu = new Student { Id = 1 };
var set = new HashSet<Student>(new StudentIdComparer()) { stu };
stu.Id = 2; // 修改Id(哈希码变化)
Console.WriteLine(set.Contains(stu)); // 输出:false(找不到)

✅ 解决:用不可变字段(如只读属性、主键)作为比较依据。

六、简化实现:匿名比较器/第三方库

方式1:使用 EqualityComparer<T>.Create(C# 6.0+)

无需定义单独的比较器类,直接通过委托创建:

// 按Id比较的匿名比较器
var idComparer = EqualityComparer<Student>.Create(
    (x, y) => x?.Id == y?.Id, // Equals逻辑
    obj => obj?.Id.GetHashCode() ?? 0 // GetHashCode逻辑
);

// 直接使用
var distinct = list1.Distinct(idComparer);

方式2:第三方库(如 MoreLINQ)

MoreLINQ 提供 DistinctBy/ExceptBy 等方法,无需手动实现比较器:

// 安装MoreLINQ:Install-Package MoreLINQ
using MoreLinq;

// 按Id去重(无需定义IEqualityComparer)
var distinct = list1.Concat(list2).DistinctBy(s => s.Id);

// 按Id求差集
var except = list1.ExceptBy(list2, s => s.Id);

七、总结

场景 实现方式
通用/可复用的比较规则 自定义 IEqualityComparer<T>
临时/简单的比较规则 EqualityComparer<T>.Create 委托
仅按单个字段比较(LINQ) MoreLINQ 的 DistinctBy/ExceptBy

核心要点:

  1. Equals 定义“相等规则”,GetHashCode 必须与之一致;
  2. 必须处理 null 场景,避免空引用异常;
  3. 优先使用不可变字段作为比较依据;
  4. 简单场景可通过委托/第三方库简化实现,无需定义类。
posted @ 2025-12-25 11:55  【唐】三三  阅读(0)  评论(0)    收藏  举报