自定义值类型一定不要忘了重写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方法是用来兜底的,一次比较两次装箱,对你的程序可是双杀哦
相关文章推荐
- 自定义值类型一定不要忘了重写Equals,否则性能和空间双双堪忧
- 自定义值类型一定不要忘了重写Equals,否则性能和空间双双堪忧
- HashSet中存方用户自定义数据类型数据,重写equals方法和hashCode方法
- 往HashSet中加入自定义类型时重写Hashcode、equals与不重新写对比
- 以码农的名义告诉你:Long类型的数据比较大小,请一定用equals,不要用==
- 理解hashmap的key是自定义类型为什么需要重写hashCode和equals
- 六.OC基础--1. id和instancetype类型,2.动态类型检测,3.响应方法,构造方法,4.重写构造方法,5.自定义构造方法
- JAVA中自定义对象比较时重写equals()方法的同时要重写hashcode()方法
- 自定义集合HashCode和equals方法重写
- angular 如果给date初始化值,model一定得是Date类型,否则会报错
- 为基类没有重写Object.Equals 方法的引用类型实现Equals
- hadoop map reduce自定义数据类型时注意顺序,否则报错。
- 如何为引用类型如何重写Object.Equals()方法?
- 用户自定义类型作为T,来实现Dictionary,HashSet时,必须重写GetHashCode 方法
- GPS坐标变换时不要将double强制转换成float类型,否则会造成精度不够
- net控件中数据导到Excel的格式 首先,我们了解一下excel从web页面上导出的原理。当我们把这些数据发送到客户端时,我们想让客户端程序(浏览器)以excel的格式读取它,所以把mime类型设为:application/vnd.ms-excel,当excel读取文件时会以每个cell的格式呈现数据,如果cell没有规定的格式,则excel会以默认的格式去呈现该cell的数据。这样就给我们提供了自定义数据格式的空间,当然我们必须使用excel支持的格式。下面就列出常用的一些格式: 1) 文本
- 使用HashSet 存储自定义对象,为什么需要重写hashCode()和equals()?
- 使用HashMap,如果key是自定义的类,就必须重写hashcode()和equals()。
- 构造函数一定不要加void等修饰符,否则这个函数不会被当做构造函数去使用,默认的构造函数是没有参数的。
- 怎么自定义windows系统的性能监视器的图形类型