这是我直到今天才注意到的。显然,当执行基于相等的操作时Tuple<T>
,经常使用的元组类(等)的 .NET 实现会导致值类型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 倍。