c# 自定义值类型一定不要忘了重写Equals,否则性能和空间双双堪忧

一:背景

1. 讲故事

曾今在项目中发现有同事自定义结构体的时候,居然没有重写Equals方法,比如下面这段代码:

  static void Main(string[] args)
  {
    var list = Enumerable.Range(0, 1000).Select(m => new Point(m, m)).ToList();
    var item = list.FirstOrDefault(m => m.Equals(new Point(int.MaxValue, int.MaxValue)));
    Console.ReadLine();
  }

  public struct Point
  {
    public int x;
    public int y;

    public Point(int x, int y)
    {
      this.x = x;
      this.y = y;
    }
  }

这代码貌似也没啥什么问题,好像大家平时也是这么写,没关系,有没有问题,跑一下再用windbg看一下。

0:000> !dumpheap -stat
Statistics:
MT Count TotalSize Class Name
00007ff8826fba20 10 16592 ConsoleApp6.Point[]
00007ff8e0055e70 6 35448 System.Object[]
00007ff8826f5b50 2000 48000 ConsoleApp6.Point

0:000> !dumpheap -mt 00007ff8826f5b50
Address MT Size
0000020d00006fe0 00007ff8826f5b50 24

0:000> !do 0000020d00006fe0
Name: ConsoleApp6.Point
Fields:
MT Field Offset Type VT Attr Value Name
00007ff8e00585a0 4000001 8 System.Int32 1 instance 0 x
00007ff8e00585a0 4000002 c System.Int32 1 instance 0 y

从上面的输出不知道你看出问题了没有? 托管堆上居然有2000个Point,而且还可以用 !do 打出来,说明这些都是引用类型。。。这些引用类型哪里来的? 看代码应该是 equals 比较时产生的,一次比较就有2个point被装箱放到托管堆上,这下惨了,,,而且大家应该知道引用对象本身还有(8+8) byte 自带开销,这在时间和空间上都是巨大的浪费呀。。。

二: 探究默认的Equals实现

1. 寻找ValueType的Equals实现

为什么会这样呢? 我们知道equals是继承自ValueType的,所以把 ValueType 翻出来看看便知:

  public abstract class ValueType
  {
    public override bool Equals(object obj)
    {
      if (CanCompareBits(this)) {return FastEqualsCheck(this, obj);}
      FieldInfo[] fields = runtimeType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
      for (int i = 0; i < fields.Length; i++)
      {
        object obj2 = ((RtFieldInfo)fields[i]).UnsafeGetValue(this);
        object obj3 = ((RtFieldInfo)fields[i]).UnsafeGetValue(obj);
        ...
      }
      return true;
    }
  }

从上面代码中可以看出有如下三点信息:

<1> 通用的 equals 方法接收object类型,参数装箱一次。

<2> CanCompareBits,FastEqualsCheck 都是采用object类型,this也需要装箱一次。

<3> 有两种比较方式,要么采用 FastEqualsCheck 比较,要么采用反射比较,我去.... 反射就玩大了。

综合来看确实没毛病, equals 会把比较的两个对象都进行装箱。

2. 改进方案

问题找到了,解决起来就简单了,不走这个通用的 equals 不就行啦,我自定义一个equals方法,然后跑一下代码。

    public bool Equals(Point other)
    {
      return this.x == other.x && this.y == other.y;
    }

可以看到走了我的自定义的Equals,🐮👃。 貌似问题就这样简单粗暴的解决了,真开心,打脸时刻开始。。。

三:真的解决问题了吗?

1. 遇到问题

很多时候我们会定义各种泛型类,在泛型操作中通常会涉及到T之间的 equals, 比如下面我设计的一段代码,为了方便,我把Point的默认Equals也重写一下。

  class Program
  {
    static void Main(string[] args)
    {

      var p1 = new Point(1, 1);
      var p2 = new Point(1, 1);

      TProxy<Point> proxy = new TProxy<Point>() { Instance = p1 };

      Console.WriteLine($"p1==p2 {proxy.IsEquals(p2)}");
      Console.ReadLine();
    }
  }

  public struct Point
  {
    public int x;
    public int y;

    public Point(int x, int y)
    {
      this.x = x;
      this.y = y;
    }

    public override bool Equals(object obj)
    {
      Console.WriteLine("我是通用的Equals");
      return base.Equals(obj);
    }

    public bool Equals(Point other)
    {
      Console.WriteLine("我是自定义的Equals");
      return this.x == other.x && this.y == other.y;
    }
  }

  public class TProxy<T>
  {
    public T Instance { get; set; }

    public bool IsEquals(T obj)
    {
      var b = Instance.Equals(obj);

      return b;
    }
  }

从输出结果看,还是走了通用的equals方法,这就尴尬了,为什么会这样呢?

2. 从FCL的值类型实现上寻找问题

有时候苦思冥想找不出问题,突然灵光一现,FCL中不也有一些自定义值类型吗? 比如 int,long,decimal,何不看它们是怎么实现的,寻找寻找灵感, 对吧。。。说干就干,把 int32 源码翻出来。

public struct Int32 : IComparable, IFormattable, IConvertible, IComparable<int>, IEquatable<int>
{
 	public override bool Equals(object obj)
	{
		if (!(obj is int))
		{
			return false;
		}
		return this == (int)obj;
	}

  public bool Equals(int obj)
	{
		return this == obj;
	}
}

我去,还是int🐮👃,貌似我的Point就比int少了接口实现,问题应该就出在这里,而且最后一个泛型接口IEquatable<int>特别显眼,看下定义:

public interface IEquatable<T>
{
	bool Equals(T other);
}

这个泛型接口也仅仅只有一个equals方法,不过灵感告诉我,貌似。。。也许。。。应该。。。就是这个泛型的equals是用来解决泛型情况下的equals比较。

3. 补上 IEquatable 接口

有了这个思路,我也跟FCL学,让Point实现 IEquatable<T>接口,然后在TProxy<T>代理类中约束下必须实现IEquatable<T>,修改代码如下:

  public struct Point : IEquatable<Point> { ... }
  public class TProxy<T> where T: IEquatable<T> { ... }

然后将程序跑起来,如下图:

🐮👃,虽然是成功了,但有一个地方让我不是很舒服,就是上面的第二行代码,在 TProxy<T> 处约束了T,因为我翻看List的实现也没做这样的泛型约束呀,可能有点强迫症吧,贴一下代码给大家看看。

public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>
{}

然后我继续模仿List,把 TProxy<T> 上的T约束去掉,结果就出问题了,又回到了 通用Equals

4. 从List的Contains源码中寻找答案

好奇心再次驱使我寻找List中是如何做到的,为了能看到List中原生方法,修改代码如下,从Contains方法入手。

  var list = Enumerable.Range(0, 1000).Select(m => new Point(m, m)).ToList();
  var item = list.Contains(new Point(int.MaxValue, int.MaxValue));

---------- outout ---------------
我是自定义的Equals
我是自定义的Equals
我是自定义的Equals
...

我也是太好奇了,翻看下 Contains 的源码,简化后实现如下。

public bool Contains(T item)
{
  ...
	EqualityComparer<T> @default = EqualityComparer<T>.Default;
	for (int j = 0; j < _size; j++)
	{
		if (@default.Equals(_items[j], item)) {return true;}
	}
	return false;
}

原来List是在进行 equals比较之前,自己构建了一个泛型比较器EqualityComparer<T>,🐮👃,然后继续追一下代码。

因为这里的runtimeType实现了IEquatable<T>接口,所以代码返回了一个泛型比较器:GenericEqualityComparer<T>,然后我们继续查看这个泛型比较器是咋样的。

从图中可以看到最终还是对T进行了IEquatable<T>约束,不过这里给提取出来了,还是挺厉害的,然后我也学的模仿一下:

可以看到也走了我的自定义实现,两种方式大家都可以用哈😁😁😁。

最后要注意一点的是,当你重写了Equals之后,编译器会告知你最好也把 GetHashCode重写一下,只是建议,如果看不惯这个提示,尽可能自定义GetHashCode方法让hashcode分布的均匀一点。

四:总结

一定要实现自定义值类型的 Equals方法,人家的 Equals方法是用来兜底的,一次比较两次装箱,对你的程序可是双杀哦😁😁😁。

以上就是c# 自定义值类型一定不要忘了重写Equals,否则性能和空间双双堪忧的详细内容,更多关于c# 自定义值类型的资料请关注我们其它相关文章!

(0)

相关推荐

  • C#中Equals方法的常见误解

    很多C#的教材都会强调对象相等的概念.我们都知道,在C#的世界里存在两种等同性.一种是逻辑等同性:如果两个对象在逻辑上代表同样的值,则称他们具有逻辑等同性.另一种是引用等同性:如果两个引用指向同一个对象实例,则称他们具有引用等同性. 众所周知,Object类型有一个名为Equals的实例方法可以用来确定两个对象是否相等.Object的Equals的默认实现比较的是两个对象的引用等同性.而Object的派生类ValueTpye重写了Equals方法,它比较的是两个对象的逻辑等同性. 也就是说,在C

  • C#使用Equals()方法比较两个对象是否相等的方法

    本文实例讲述了C#使用Equals()方法比较两个对象是否相等的方法.分享给大家供大家参考.具体如下: int price = 100; int amount = 1000 if (price.Equals(amount)) { Console.WriteLine("Price is equal to amount"); } else { Console.WriteLine("Price is not equal to amount"); } 希望本文所述对大家的C

  • 详解C#中==、Equals、ReferenceEquals的区别

    本文导读: C#中Equals , == , ReferenceEquals都可以用于判断两个对象的个体是不是相等,对于相同的基本值类型,==和Equals()比较结果是一样的:由于ReferenceEquals()是判断两个对象的引用是否相等,对于值类型,因为每次判断前都必须进行装箱操作,也就是每次都生成了一个临时的object,因而永远返回false. 一.== 运算符 1.静态相等符号,对应存在的!=,这个符号是一个可以重载的二元操作符,可以用于比较两个对象是否相等. 2.它会根据需要自动

  • C#中的Equals、RefrenceEquals和==的区别与联系

    C#中判断两个对象是否相等有Equals.RefrenceEquals和==三种,其中==为运算符,其它两个为方法,而Equals又有两种版本,一个是静态的,一个是虚拟的,虚拟的可以被实体类重写,静态的在方法体内也是调用虚拟的,如下: 复制代码 代码如下: public static bool Equals(object objA, object objB) { return ((objA == objB) || (((objA != null) && (objB != null)) &a

  • C#中的 == 和equals()区别浅析

    首先看看,如以下代码: int age = 25; short newAge = 25; Console.WriteLine(age == newAge); //true Console.WriteLine(newAge.Equals(age)); //false Console.ReadLine(); int和short为原始类型,但与"=="比较返回true,equals()比较返回false.为什么呢? 简而言之:"equals()"相比"= =&q

  • C#值类型、引用类型中的Equals和==的区别浅析

    引言 最近一个朋友正在找工作,他说在笔试题中遇到Equals和==有什么区别的题,当时跟他说如果是值类型的,它们没有区别,如果是引用类型的有区别,但string类型除外.为了证实自己的说法,也研究了一下,以免误导别人,这里将研究结果总结一下,如果我有什么地方说的不对的地方,望指出. 相等性 在定义类或结构时,您将决定为类型创建值相等性(或等效性)的自定义定义是否有意义. 通常,当类型的对象预期要添加到某类集合时,或者当这些对象主要用于存储一组字段或属性时,您将实现值相等性. 您可以基于类型中所有

  • C#基础:Equals()与运算符==的区别分析

    对于值类型,如果对象的值相等,则相等运算符 (==) 返回 true,否则返回 false.对于string 以外的引用类型,如果两个对象引用同一个对象,则 == 返回 true.对于 string 类型,== 比较字符串的值.==操作比较的是两个变量的值是否相等.equals()方法比较的是两个对象的内容是否一致.equals也就是比较引用类型是否是对同一个对象的引用.对于值类型的比较,这里就不做描述了,下面讨论引用类型的比较:首先我们看一段程序 复制代码 代码如下: using System

  • C#中Equals和GetHashCode使用及区别

    Equals和GetHashCode Equals每个实现都必须遵循以下约定: 自反性(Reflexive): x.equals(x)必须返回true. 对称性(Symmetric): x.equals(y)为true时,y.equals(x)也为true. 传递性(Transitive): 对于任何非null的应用值x,y和z,如果x.equals(y)返回true,并且y.equals(z)也返回true,那么x.equals(z)必须返回true. 一致性(Consistence): 如果

  • c# 自定义值类型一定不要忘了重写Equals,否则性能和空间双双堪忧

    一:背景 1. 讲故事 曾今在项目中发现有同事自定义结构体的时候,居然没有重写Equals方法,比如下面这段代码: static void Main(string[] args) { var list = Enumerable.Range(0, 1000).Select(m => new Point(m, m)).ToList(); var item = list.FirstOrDefault(m => m.Equals(new Point(int.MaxValue, int.MaxValue

  • c#中值类型和引用类型的基础教程

    前言 值类型和引用类型,是c#比较基础,也必须掌握的知识点,但是也不是那么轻易就能掌握,今天跟着老胡一起来看看吧. 典型类型 首先我们看看这两种不同的类型有哪些比较典型的代表. 典型值类型 int, long, float, double等原始类型中表示数字的类型都是值类型,表示时间的datatime也是值类型,除此之外我们还可以通过关键字struct自定义值类型. 典型引用类型 原始类型中,array, list, dictionary, queue, stack和string都是引用类型,除

  • Java泛型映射不同的值类型详解及实例代码

    Java泛型映射不同的值类型详解 前言: 一般来说,开发人员偶尔会遇到这样的情形: 在一个特定容器中映射任意类型的值.然而Java 集合API只提供了参数化的容器.这限制了类型安全地使用HashMap,如单一的值类型.但如果想混合苹果和梨,该怎样做呢? 幸运的是,有一个简单的设计模式允许使用Java泛型映射不同的值类型,Joshua Bloch在其<Effective Java>(第二版,第29项)中将其描述为类型安全的异构容器(typesafe hetereogeneous Containe

  • Spring MVC自定义日期类型转换器实例详解

    Spring MVC自定义日期类型转换器实例详解 WEB层采用Spring MVC框架,将查询到的数据传递给APP端或客户端,这没啥,但是坑的是实体类中有日期类型的属性,但是你必须提前格式化好之后返回给它们.说真的,以前真没这样做过,之前都是一口气查询到数据,然后在jsp页面上格式化,最后展示给用户.但是这次不同,这次我纯属操作数据,没有页面.直接从数据库拿数据给它们返数据.它们给我传数据我持久化数据,说到这里一个小问题就默默的来了. 首先把问题还原一下吧(这是一个数据导出功能),下图中用红框圈

  • PowerShell函数指定返回值类型实例

    本文介绍在自定义PowerShell函数时,如何设置返回值的数据类型.PowerShell函数的返回值可以有类型,也可以没有类型,跟输入参数相似.     定义PowerShell函数的返回值类型,要使用OutputType这个指令.将这个指令放到param指令之前即可实现对返回值类型的定义. 复制代码 代码如下: function Test-IntelliSense {     [OutputType('System.DateTime')]     param()     return Get

  • Django ORM 自定义 char 类型字段解析

    用 CharField 定义的字段在数据库中存放为 verchar 类型 自定义 char 类型字段需要下面的代码: class FixedCharField(models.Field): """ 自定义的 char 类型的字段类 """ def __init__(self, max_length, *args, **kwargs): self.max_length = max_length super(FixedCharField, self)

  • C# WebApi 接口返回值不困惑:返回值类型详解

    前言:已经有一个月没写点什么了,感觉心里空落落的.今天再来篇干货,想要学习Webapi的园友们速速动起来,跟着博主一起来学习吧.之前分享过一篇C#进阶系列--WebApi接口传参不再困惑:传参详解,这篇博文内容本身很基础,没想到引起很多园友关注,感谢大家的支持.作为程序猿,我们都知道参数和返回值是编程领域不可分割的两大块,此前分享了下WebApi的传参机制,今天再来看看WebApi里面另一个重要而又基础的知识点:返回值.还是那句话:本篇针对初初使用WebApi的同学们,比较基础,有兴趣的且看看.

  • JavaScript对象内置对象,值类型和引用类型讲解

    目录 对象 对象的定义 遍历对象的成员 JS内置对象 Math对象 Date对象 数组对象 String对象 值类型和引用类型 对象 JS中的对象是属性和行为的结合体,其中属性是对象的静态特征,行为又称方法,是对象的动态特征. JavaScript中的对象主要分为三大类: 内置对象 由ES标准中定义的对象 在任何的ES的实现中都可以使用,比如Math String Number Boolean Function Object 宿主对象 由JS的运行环境提供的对象,目前来讲主要是浏览器提供的对象,

  • C语言编程C++自定义个性化类型

    目录 自定义类型 结构体 声明一个结构体类型 特殊声明 结构体自引用 结构体变量的定义和初始化 结构体内存对齐 结构体传参 位段 枚举 联合(共用体) 联合类型的定义 联合类型的声明 联合的特点 判断当前机器的大小端存储[] 自定义类型 结构体 结构是一些值的集合,这些值称为成员变量.结构的每个成员可以是不同类型的变量 声明一个结构体类型 //声明一个学生类型,是想通过学生类型来创建学生变量(对象) //描述学生就得有属性啥的.名字,电话,性别,年龄 struct Stu { char name

随机推荐