您的位置:首页 > 其它

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

2021-02-24 21:57 501 查看

一:背景

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
方法是用来兜底的,一次比较两次装箱,对你的程序可是双杀哦


内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐