我有一个项目,我正在广泛使用通用C#dictionary。我需要复合键,所以我一直使用tuples作为键。在某些时候,我想知道使用缓存哈希码的自定义类是否有益:
public class CompositeKey<T1, T2> : Tuple<T1, T2>
{
private readonly int _hashCode;
public CompositeKey(T1 t1, T2 t2) : base(t1, t2)
{
_hashCode = base.GetHashCode();
}
public new int GetHashCode()
{
return _hashCode;
}
}
我使用new
代替override
,因为我认为这对于此测试没有任何影响,因为我使用具体类型定义字典:
var dict = new Dictionary<CompositeKey<string, int>, int>();
但是,我注意到我的自定义GetHashCode
方法根本没有被调用。当我将new
更改为override
时,它会按预期调用。
有人可以解释为什么不调用隐藏的GetHashCode
方法吗?如果我像这样定义字典
var dict = new Dictionary<Tuple<string, int>, int>();
但如果我在示例中明确指定CompositeKey
类型,则不行。
P.S。我知道隐藏GetHashCode
方法可能不是一个好主意。
答案 0 :(得分:10)
有人可以解释为什么不调用隐藏的GetHashCode方法吗? 如果我要定义字典,我会期待这种行为 此
为了能够调用CompositeKey.GetHashCode
方法,必须在编译时将CompositeKey
实例的引用输入为CompositeKey
。
但Dictionary<TKey,TValue>
的代码库并不知道您的CompositeKey
类(显然)。它所知道的只是TKey
(泛型类型参数),它与没有任何约束的System.Object
等价。因为您无法在没有约束的情况下调用T
中声明的System.Object
以外的任何方法。
因此,字典最终会调用Object.GetHashCode
而不会在您的类中重写 - 因此它不会被调用。
答案 1 :(得分:3)
泛型类型中方法调用的重载解析在未编译的泛型类型(例如Dictionary<TKey, TValue>
)编译时发生,而不是在构造闭合类型(例如Dictionary<CompositeKey<string, int>, int>
)时发生。
由于TKey
中的Dictionary<,>
没有约束,GetHashCode()
唯一可用的重载是object.GetHashCode()
。构造具有更好的GetHashCode()
重载的类型不会改变初始重载决策。
它不仅限于隐藏new
的方法。重载方法也是如此:
class Generic<T>
{
public bool Equal(T t1, T t2)
{
return t1.Equals(t2);
}
}
class X : IEquatable<X>
{
public override bool Equals(object obj)
{
Console.WriteLine("object.Equals");
return true;
}
public bool Equals(X other)
{
Console.WriteLine("IEquatable.Equals");
return true;
}
}
永远不会使用X.Equals(X)
重载
var test = new Generic<X>();
test.Equal(new X(), new X());
// prints "object.Equals"
答案 2 :(得分:1)
只是详细说明以前的答案。使用new的问题是它只会覆盖方法,如果消费者直接在类上操作(在本例中是你的CompositeKey类)。对你的CompositeKey派生的任何基类的任何调用都不会调用你的新成员。
所以,如果在下面:
正如先前的答案所强调的那样,因为EqualityComparer(类字典使用)指定T是非约束泛型,那么编译器将仅支持所有T 的最小公分母传递给它,这是直接在Object上的方法。
因此调用是有效的:((Object)key)。GetHashCode()。从上面你可以看到这不会调用你的新方法。
答案 3 :(得分:0)
这是因为泛型的类型限制。这是一个简化的程序来显示问题。
public class Program
{
public static void Main(string[] args)
{
var bar = new Bar();
TestMethod(bar);
TestMethod2(bar);
}
public static void TestMethod<T>(T obj) where T : Foo
{
obj.Test();
obj.Test2();
}
public static void TestMethod2<T>(T obj) where T : Bar
{
obj.Test();
obj.Test2();
}
}
public class Foo
{
public virtual void Test()
{
Debugger.Break();
}
public virtual void Test2()
{
Debugger.Break();
}
}
public class Bar : Foo
{
public new void Test()
{
Debugger.Break();
}
public override void Test2()
{
Debugger.Break();
}
}
在TestMethod()
中,您点击了Foo.Test()
和Bar.Test2()
中的断点,但在TestMethod2()
中,您点击了Bar.Test()
和Bar.Test2()
中的断点,是因为在第一种方法中你被约束为Foo
或更低的类型,所以当编译器编译时它绑定到Foo
上的调用,就像函数写成
public static void TestMethod<T>(T obj)
{
((Foo)obj).Test(); //You would expect this to call Foo.Test() b\c of shadowing
((Foo)obj).Test2(); //You would expect this to call Bar.Test2() b\c of overloading
}
现在,关于您的问题,正在使用的比较器是written as
[Serializable]
internal class ObjectEqualityComparer<T>: EqualityComparer<T>
{
[Pure]
public override bool Equals(T x, T y) {
if (x != null) {
if (y != null) return x.Equals(y);
return false;
}
if (y != null) return false;
return true;
}
[Pure]
public override int GetHashCode(T obj) {
if (obj == null) return 0;
return obj.GetHashCode();
}
//...
}
T
没有约束,因此这两种方法的行为就好像它们被写成
public override bool Equals(T x, T y) {
if (x != null) {
if (y != null) return ((object)x).Equals(y);
return false;
}
if (y != null) return false;
return true;
}
[Pure]
public override int GetHashCode(T obj) {
if (obj == null) return 0;
return ((object)obj).GetHashCode();
}
这就是为什么你的功能只在你覆盖它时调用,而不是在你遮蔽它时调用。