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.Point0:000> !dumpheap -mt 00007ff8826f5b50
Address MT Size
0000020d00006fe0 00007ff8826f5b50 240: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# 自定义值类型的资料请关注猪先飞其它相关文章!
相关文章
- 我们在使用C#做项目的时候,基本上都需要制作登录界面,那么今天我们就来一步步看看,如果简单的实现登录界面呢,本文给出2个例子,由简入难,希望大家能够喜欢。...2020-06-25
- 这篇文章主要介绍了C# 字段和属性的的相关资料,文中示例代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下...2020-11-03
- 这篇文章主要介绍了C#中截取字符串的的基本方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-11-03
- 本文给大家分享C#连接SQL数据库和查询数据功能的操作技巧,本文通过图文并茂的形式给大家介绍的非常详细,需要的朋友参考下吧...2021-05-17
- 这篇文章主要介绍了C#实现简单的Http请求的方法,以实例形式较为详细的分析了C#实现Http请求的具体方法,需要的朋友可以参考下...2020-06-25
- 本文主要介绍了C#中new的几种用法,具有很好的参考价值,下面跟着小编一起来看下吧...2020-06-25
使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序)
这篇文章主要介绍了使用Visual Studio2019创建C#项目(窗体应用程序、控制台应用程序、Web应用程序),小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25- 这篇文章主要介绍了C#开发Windows窗体应用程序的简单操作步骤,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-04-12
- 这篇文章主要介绍了C#从数据库读取图片并保存的方法,帮助大家更好的理解和使用c#,感兴趣的朋友可以了解下...2021-01-16
- 最近做一个小项目不可避免的需要前端脚本与后台进行交互。由于是在asp.net中实现,故问题演化成asp.net中jiavascript与后台c#如何进行交互。...2020-06-25
- 这篇文章主要用实例讲解C#递归算法的概念以及用法,文中代码非常详细,帮助大家更好的参考和学习,感兴趣的朋友可以了解下...2020-06-25
- 本文通过例子,讲述了C++调用C#的DLL程序的方法,作出了以下总结,下面就让我们一起来学习吧。...2020-06-25
- 轻松学习C#的基础入门,了解C#最基本的知识点,C#是一种简洁的,类型安全的一种完全面向对象的开发语言,是Microsoft专门基于.NET Framework平台开发的而量身定做的高级程序设计语言,需要的朋友可以参考下...2020-06-25
- 本文主要介绍了C#变量命名规则小结,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2021-09-09
- 这篇文章主要介绍了c#中(&&,||)与(&,|)的区别详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-06-25
- 这篇文章主要介绍了C#绘制曲线图的方法,以完整实例形式较为详细的分析了C#进行曲线绘制的具体步骤与相关技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25
- 本文主要介绍了C# 中取绝对值的函数。具有很好的参考价值。下面跟着小编一起来看下吧...2020-06-25
- 这篇文章主要介绍了c#自带缓存使用方法,包括获取数据缓存、设置数据缓存、移除指定数据缓存等方法,需要的朋友可以参考下...2020-06-25
- 下面小编就为大家带来一篇C#学习笔记- 随机函数Random()的用法详解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧...2020-06-25
- 这篇文章主要介绍了C#中list用法,结合实例形式分析了C#中list排序、运算、转换等常见操作技巧,具有一定参考借鉴价值,需要的朋友可以参考下...2020-06-25