惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
P
Palo Alto Networks Blog
T
ThreatConnect
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
T
True Tiger Recordings
P
Privacy & Cybersecurity Law Blog
B
Blog
IT之家
IT之家
Last Week in AI
Last Week in AI
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
C
Comments on: Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
N
News and Events Feed by Topic
NISL@THU
NISL@THU
腾讯CDC
雷峰网
雷峰网
Security Latest
Security Latest
李成银的技术随笔
M
Microsoft Research Blog - Microsoft Research
L
LangChain Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
博客园 - Franky
N
News | PayPal Newsroom
V
V2EX
A
About on SuperTechFans
The Register - Security
The Register - Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
爱范儿
爱范儿
A
Arctic Wolf
L
LINUX DO - 最新话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

博客园 - 陈大欠

Step Builder:让对象构建有顺序、有边界、有校验 基于 Attribute 的 AOP 字段校验 浅析 .NET 响应式编程 IObservable与ReactiveX Cron表达式简明教程 正则表达式拾遗 快速批量升级 NugetPackage 版本 使用XDT提高开发效率 如何保证XML正确性 分析 Dump 入门简明教程 Git commit emoji 对照表 ELK Stack 笔记 HTML5/CSS3(PrefixFree.js) 3D文字特效 Jquery实现图片上下一张 js css 构建滚动边框 使用jquery构建Metro style 返回顶部 使用js实现移动设备访问跳转到指定目录 从一幅图中了解开源世界 Jquery ajax 学习笔记 工欲善其事必先利其器系列之:在VS里面折叠js代码
C# IEquatable和IEqualityComparer 最佳实践
陈大欠 · 2024-01-04 · via 博客园 - 陈大欠

前言

IEquatable<T>IEqualityComparer<T>C# 中用于比较对象的接口,它们有以下区别:

  • IEquatable
    IEquatable<T> 是一个泛型接口,定义了一个用于比较对象相等性的方法 Equals(T other)
    当你想要在类中自定义相等性比较的逻辑时,可以实现 IEquatable<T> 接口。
    实现了 IEquatable<T> 接口的类可以通过调用 Equals 方法来进行对象之间的相等性比较。
  • IEqualityComparer
    IEqualityComparer<T> 也是一个泛型接口,定义了两个方法:Equals(T x, T y)GetHashCode(T obj)
    当你需要自定义对象之间的相等性比较逻辑和哈希码计算逻辑时,可以实现 IEqualityComparer<T> 接口。

通常场景他们使用效果是一样的,例如进行Enumerable.Distinct

IEquatable<T>

IEquatable<T>C# 中用于比较对象相等性的泛型接口。它定义了一个名为 Equals 的方法,用于比较对象与另一个对象的相等性。

通过实现 IEquatable<T> 接口,你可以在类中自定义对象相等性的比较逻辑。这使得你的类更具灵活性,可以根据你的需求来确定两个对象是否相等。

根据MS推荐实现了Equals最好也实现他的操作符==!=

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

static class Example
{
    static void Main()
    {
        List<Person> personList = new List<Person>()
        {
            new Person() { Name = "A", Age = 1,  },
            new Person() { Name = "B", Age = 2,  },
            new Person() { Name = "B", Age = 2,  },
            new Person() { Name = "C", Age = 3,  },
            new Person() { Name = "C", Age = 3,  },
        };

        IEnumerable<Person> nameDisList = personList.Distinct(); // 实现了 IEquatable 按照 Name + Age 进行去重

        // {Name=A, Age=1},{Name=B, Age=2},{Name=C, Age=3}
        Console.WriteLine(string.Join(",", nameDisList));

        Console.ReadKey();
    }
}

public class Person : IEquatable<Person>
{
    public int Age
    {
        get;
        set;
    }

    public string Name
    {
        get;
        set;
    }

    public override bool Equals(object obj)
    {
        return this.Equals(obj as Person); // 调用内部的对比即可
    }

    public bool Equals(Person other)
    {
        if (other == null)
            return false;

        // Optimization for a common success case.
        if (ReferenceEquals(this, other))
            return true;

        if (this.Age == other.Age && this.Name == other.Name)
            return true;
        else
            return false;
    }

    public override int GetHashCode()
    {
        // 选择两个不同的质数,例如 17 和 23
        // 关于质数,可以参考 https://en.wikipedia.org/wiki/Prime_number
        // 重写 GetHashCode() 方法最佳实践请见 https://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-overriding-gethashcode
        int hashCode = 17;
        hashCode = hashCode * 23 + EqualityComparer<string>.Default.GetHashCode(this.Name);
        hashCode = hashCode * 23 + EqualityComparer<int>.Default.GetHashCode(this.Age);

        return hashCode;
    }

    public static bool operator ==(Person p1, Person p2)
    {
        if (p1 is null && p2 is null) // 一定不要操作符重写中使用 == 操作符来判断两个对象是否相等,否则会堆栈溢出
            return true;

        if (((object)p1) == null || ((object)p2) == null)
            return Equals(p1, p2);

        return p1.Equals(p2);
    }

    public static bool operator !=(Person p1, Person p2)
    {
        return !(p1 == p2); // 使用不等于即可
    }
}

IEqualityComparer<T>

IEqualityComparer<T>C# 中用于比较对象相等性的泛型接口。它定义了两个方法:Equals(object x, object y)GetHashCode(object obj)

Equals(object x, object y) 方法用于比较两个对象 xy 是否相等。它返回一个布尔值,指示两个对象是否相等。
GetHashCode(object obj) 方法用于计算对象 obj 的哈希码。哈希码是一个整数,用于在哈希表等数据结构中进行快速查找和比较。
通常情况下,IEqualityComparer 接口用于在没有泛型的情况下,提供自定义的对象相等性比较和哈希码计算逻辑。 你可以实现 IEqualityComparer 接口来定义你自己的比较器,以便在需要自定义对象相等性比较的情况下使用。

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

static class Example
{
    static void Main()
    {
        List<Person> personList = new List<Person>()
        {
            new Person() { Name = "A", Age = 1,  },
            new Person() { Name = "B", Age = 2,  },
            new Person() { Name = "B", Age = 3,  },
            new Person() { Name = "C", Age = 4,  },
            new Person() { Name = "B", Age = 4,  },
        };

        IEnumerable<Person> nameDisList = personList.Distinct(new PersonNameEqualityComparer());
        IEnumerable<Person> ageDisList = personList.Distinct(new PersonAgeEqualityComparer());

        // {Name=A, Age=1},{Name=B, Age=2},{Name=C, Age=4}
        Console.WriteLine(string.Join(",", nameDisList));
        // {Name = A, Age = 1},{ Name = B, Age = 2},{ Name = B, Age = 3},{ Name = C, Age = 4}
        Console.WriteLine(string.Join(",", ageDisList));

        Console.ReadKey();
    }
}

class PersonNameEqualityComparer : IEqualityComparer<Person>
{
    public bool Equals(Person p1, Person p2)
    {
        if (ReferenceEquals(p1, p2))
            return true;
        if (p2 is null || p1 is null)
            return false;

        return p1.Name == p2.Name;
    }

    public int GetHashCode(Person p) => p.Name.GetHashCode();
}

class PersonAgeEqualityComparer : IEqualityComparer<Person>
{
    public bool Equals(Person p1, Person p2)
    {
        if (ReferenceEquals(p1, p2))
            return true;
        if (p2 is null || p1 is null)
            return false;
            
        return p1.Age == p2.Age;
    }

    public int GetHashCode(Person p) => p.Age.GetHashCode();
}

MS不建议直接派生IEqualityComparer<T>,而是使用EqualityComparer<T>,本例代码可以直接改成EqualityComparer

建议从 EqualityComparer 类派生,而不是实现 IEqualityComparer 接口,因为 EqualityComparer 类使用 IEquatable.Equals 方法而不是 Object.Equals 方法测试相等性。 这与 Contains类和其他泛型集合的 Dictionary<TKey,TValue> 、IndexOf、 LastIndexOf和 Remove 方法一致。

如果是单纯的去重,高版本的C#可以使用Enumerable.DistinctBy

后言

通过上面介绍我们了解了它们两种的使用方式,那么什么时候用IEqualityComparer<T> 什么时候用IEquatable<T>呢?

  1. 当目标类你没有源代码,无法直接修改的时候请使用IEqualityComparer<T>
  2. 当目标类你只需要一种对比方式可以考虑使用IEquatable<T>
  3. 当目标类有多重对比方式,例如根据AB或者BC等多重属性进行对比,你可以定义多个IEqualityComparer<T>
  4. 当然两种方式可以共存,根据需要使用

关于 HashCode

对于GetHashCode方法来说我们即使返回的是不同的值,但是也有可能会出现哈希冲突,所以我们需要尽量减少哈希冲突的概率,这样可以提高哈希表的性能。
即使你返回相同的值,最终效果也是一样的,因为它都会走一遍对比逻辑,性能会低很多。

对于高版本可以使用ValueTuple,值元祖不会在堆上创建垃圾对象。
低版本可以使用匿名类型,使用匿名类型的原始技术会在堆上创建一个对象,也就是垃圾,因为匿名类型是作为类实现的。

// ValueTuple
(PropA, PropB, PropC, PropD).GetHashCode();

// Anonymous Type
new { PropA, PropB, PropC, PropD }.GetHashCode();

HashCode最佳实践可以参考stackoverflow这篇文章。

参考