直到今天我才注意到的一件事情。显然,.NET实现的常用元组类(Tuple<T>
,Tuple<T1, T2>
等)在执行基于相等性的操作时会对值类型产生装箱惩罚。
以下是该类在框架中的实现方式(通过ILSpy获取源代码):
public class Tuple<T1, T2> : IStructuralEquatable
{
public T1 Item1 { get; private set; }
public T2 Item2 { get; private set; }
public Tuple(T1 item1, T2 item2)
{
this.Item1 = item1;
this.Item2 = item2;
}
public override bool Equals(object obj)
{
return this.Equals(obj, EqualityComparer<object>.Default);
}
public override int GetHashCode()
{
return this.GetHashCode(EqualityComparer<object>.Default);
}
public bool Equals(object obj, IEqualityComparer comparer)
{
if (obj == null)
{
return false;
}
var tuple = obj as Tuple<T1, T2>;
return tuple != null
&& comparer.Equals(this.Item1, tuple.Item1)
&& comparer.Equals(this.Item2, tuple.Item2);
}
public int GetHashCode(IEqualityComparer comparer)
{
int h1 = comparer.GetHashCode(this.Item1);
int h2 = comparer.GetHashCode(this.Item2);
return (h1 << 5) + h1 ^ h2;
}
}
我看到的问题是它会导致两个阶段的装箱和拆箱,例如对于Equals
调用,第一步在comparer.Equals
处将项目装箱,第二步,EqualityComparer<object>
调用非泛型Equals
,这将内部转而必须将项目拆箱为原始类型。
为什么他们不做像这样的事情呢:
public override bool Equals(object obj)
{
var tuple = obj as Tuple<T1, T2>;
return tuple != null
&& EqualityComparer<T1>.Default.Equals(this.Item1, tuple.Item1)
&& EqualityComparer<T2>.Default.Equals(this.Item2, tuple.Item2);
}
public override int GetHashCode()
{
int h1 = EqualityComparer<T1>.Default.GetHashCode(this.Item1);
int h2 = EqualityComparer<T2>.Default.GetHashCode(this.Item2);
return (h1 << 5) + h1 ^ h2;
}
public bool Equals(object obj, IEqualityComparer comparer)
{
var tuple = obj as Tuple<T1, T2>;
return tuple != null
&& comparer.Equals(this.Item1, tuple.Item1)
&& comparer.Equals(this.Item2, tuple.Item2);
}
public int GetHashCode(IEqualityComparer comparer)
{
int h1 = comparer.GetHashCode(this.Item1);
int h2 = comparer.GetHashCode(this.Item2);
return (h1 << 5) + h1 ^ h2;
}
我对在.NET元组类中这样实现平等感到惊讶。我将元组类型用作字典中的键。
有没有什么理由必须按照第一个代码所示进行实现?在这种情况下使用该类有点令人沮丧。
我认为代码重构和非重复数据不应该是主要问题。相同的非泛型/装箱实现也适用于IStructuralComparable
,但由于IStructuralComparable.CompareTo
不常用,因此通常不会出现问题。
我用第三种方法(只包含基本要素)对上述两种方法进行了基准测试,这种方法仍然较少负荷:
public override bool Equals(object obj)
{
return this.Equals(obj, EqualityComparer<T1>.Default, EqualityComparer<T2>.Default);
}
public bool Equals(object obj, IEqualityComparer comparer)
{
return this.Equals(obj, comparer, comparer);
}
private bool Equals(object obj, IEqualityComparer comparer1, IEqualityComparer comparer2)
{
var tuple = obj as Tuple<T1, T2>;
return tuple != null
&& comparer1.Equals(this.Item1, tuple.Item1)
&& comparer2.Equals(this.Item2, tuple.Item2);
}
对于一些Tuple<DateTime,DateTime>
字段进行了1000000次Equals
调用,这是结果:
第一种方法(原始.NET实现)- 310毫秒
第二种方法 - 60毫秒
第三种方法 - 130毫秒
默认实现比最优解慢4-5倍。
IEqualityComparer<object>
,但我并不是说没有理由。但你仍然可以让它更好一些:http://pastebin.com/tNA2FYjq - MarcinJuraszekTuple<,>
实现了IStructuralEquatable
,其中包含以下定义:bool Equals(object other, IEqualityComparer comparer); int GetHashCode(IEqualityComparer comparer);
- nawfalTuple
),并审查了所有使用复合字典的情况。我们采取了两种方法。1)在字典中使用复合键,由自定义类或结构体制成,类型本身或字典上具有自定义相等性。2)嵌套字典的复合字典结构。根据键宽度创建很多字典,但访问非常快。性能得到了提高。 - Adam HouldsworthTuple
默认代码可能不太理想,但除非编写/批准代码的人出现,否则你不太可能理解导致结果实现背后的动机。 - Adam Houldsworth