tnblog
首页
视频
资源
登录

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

1008人阅读 2024/9/20 16:05 总访问:3467564 评论:0 收藏:0 手机
分类: .net后台框架

.netcore

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

一:背景

1. 讲故事


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

  1. static void Main(string[] args)
  2. {
  3. var list = Enumerable.Range(0, 1000).Select(m => new Point(m, m)).ToList();
  4. var item = list.FirstOrDefault(m => m.Equals(new Point(int.MaxValue, int.MaxValue)));
  5. Console.ReadLine();
  6. }
  7. public struct Point
  8. {
  9. public int x;
  10. public int y;
  11. public Point(int x, int y)
  12. {
  13. this.x = x;
  14. this.y = y;
  15. }
  16. }


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

  1. 0:000> !dumpheap -stat
  2. Statistics:
  3. MT Count TotalSize Class Name
  4. 00007ff8826fba20 10 16592 ConsoleApp6.Point[]
  5. 00007ff8e0055e70 6 35448 System.Object[]
  6. 00007ff8826f5b50 2000 48000 ConsoleApp6.Point
  7. 0:000> !dumpheap -mt 00007ff8826f5b50
  8. Address MT Size
  9. 0000020d00006fe0 00007ff8826f5b50 24
  10. 0:000> !do 0000020d00006fe0
  11. Name: ConsoleApp6.Point
  12. Fields:
  13. MT Field Offset Type VT Attr Value Name
  14. 00007ff8e00585a0 4000001 8 System.Int32 1 instance 0 x
  15. 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翻出来看看便知:

  1. public abstract class ValueType
  2. {
  3. public override bool Equals(object obj)
  4. {
  5. if (CanCompareBits(this)) {return FastEqualsCheck(this, obj);}
  6. FieldInfo[] fields = runtimeType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
  7. for (int i = 0; i < fields.Length; i++)
  8. {
  9. object obj2 = ((RtFieldInfo)fields[i]).UnsafeGetValue(this);
  10. object obj3 = ((RtFieldInfo)fields[i]).UnsafeGetValue(obj);
  11. ...
  12. }
  13. return true;
  14. }
  15. }


从上面代码中可以看出有如下三点信息:
1.通用的 equals 方法接收object类型,参数装箱一次。
2.CanCompareBits,FastEqualsCheck 都是采用object类型,this也需要装箱一次。


有两种比较方式,要么采用FastEqualsCheck比较,要么采用反射比较,我去…. 反射就玩大了。
综合来看确实没毛病, equals 会把比较的两个对象都进行装箱。

2. 改进方案


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

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


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

三:真的解决问题了吗?

1. 遇到问题


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

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var p1 = new Point(1, 1);
  6. var p2 = new Point(1, 1);
  7. TProxy<Point> proxy = new TProxy<Point>() { Instance = p1 };
  8. Console.WriteLine($"p1==p2 {proxy.IsEquals(p2)}");
  9. Console.ReadLine();
  10. }
  11. }
  12. public struct Point
  13. {
  14. public int x;
  15. public int y;
  16. public Point(int x, int y)
  17. {
  18. this.x = x;
  19. this.y = y;
  20. }
  21. public override bool Equals(object obj)
  22. {
  23. Console.WriteLine("我是通用的Equals");
  24. return base.Equals(obj);
  25. }
  26. public bool Equals(Point other)
  27. {
  28. Console.WriteLine("我是自定义的Equals");
  29. return this.x == other.x && this.y == other.y;
  30. }
  31. }
  32. public class TProxy<T>
  33. {
  34. public T Instance { get; set; }
  35. public bool IsEquals(T obj)
  36. {
  37. var b = Instance.Equals(obj);
  38. return b;
  39. }
  40. }


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

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


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

  1. public struct Int32 : IComparable, IFormattable, IConvertible, IComparable<int>, IEquatable<int>
  2. {
  3. public override bool Equals(object obj)
  4. {
  5. if (!(obj is int))
  6. {
  7. return false;
  8. }
  9. return this == (int)obj;
  10. }
  11. public bool Equals(int obj)
  12. {
  13. return this == obj;
  14. }
  15. }


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

  1. public interface IEquatable<T>
  2. {
  3. bool Equals(T other);
  4. }


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

3. 补上 IEquatable 接口


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

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


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


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

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


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

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


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

  1. var list = Enumerable.Range(0, 1000).Select(m => new Point(m, m)).ToList();
  2. var item = list.Contains(new Point(int.MaxValue, int.MaxValue));
  3. ---------- outout ---------------
  4. 我是自定义的Equals
  5. 我是自定义的Equals
  6. 我是自定义的Equals
  7. ...


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

  1. public bool Contains(T item)
  2. {
  3. ...
  4. EqualityComparer<T> @default = EqualityComparer<T>.Default;
  5. for (int j = 0; j < _size; j++)
  6. {
  7. if (@default.Equals(_items[j], item)) {return true;}
  8. }
  9. return false;
  10. }


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


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


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


可以看到也走了我的自定义实现,两种方式大家都可以用哈。
最后要注意一点的是,当你重写了Equals之后,编译器会告知你最好也把GetHashCode重写一下,只是建议,如果看不惯这个提示,尽可能自定义GetHashCode方法让hashcode分布的均匀一点。

四:总结


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


欢迎加群讨论技术,1群:677373950(满了,可以加,但通过不了),2群:656732739

评价
这一世以无限游戏为使命!
排名
2
文章
634
粉丝
44
评论
93
docker中Sware集群与service
尘叶心繁 : 想学呀!我教你呀
一个bug让程序员走上法庭 索赔金额达400亿日元
叼着奶瓶逛酒吧 : 所以说做程序员也要懂点法律知识
.net core 塑形资源
剑轩 : 收藏收藏
映射AutoMapper
剑轩 : 好是好,这个对效率影响大不大哇,效率高不高
ASP.NET Core 服务注册生命周期
剑轩 : http://www.tnblog.net/aojiancc2/article/details/167
ICP备案 :渝ICP备18016597号-1
网站信息:2018-2025TNBLOG.NET
技术交流:群号656732739
联系我们:contact@tnblog.net
公网安备:50010702506256
欢迎加群交流技术