![](/img/trans.png)
[英]Overriding Equals and GetHashCode - default implementation in derived class
[英]Equals/GetHashCode override warning in derived class with no state
我为流经我们系统的各种字符串 ID 创建了一个强类型、不可变的包装类
(为简洁起见,省略了一些错误检查和格式设置...)
public abstract class BaseId
{
// Gets the type name of the derived (concrete) class
protected abstract string TypeName { get; }
protected internal string Id { get; private set; }
protected BaseId(string id) { Id = id; }
// Called by T.Equals(T) where T is a derived type
protected bool Equals(BaseId other)
{
if (ReferenceEquals(null, other))
return false;
if (ReferenceEquals(this, other))
return true;
return String.Equals(Id, other.Id);
}
// warning CS0660 (see comment #1 below)
//public override bool Equals(object obj) { return base.Equals(obj); }
public override int GetHashCode()
{
return TypeName.GetHashCode() * 17 + Id.GetHashCode();
}
public override string ToString()
{
return TypeName + ":" + Id;
}
// All T1 == T2 comparisons come here (where T1 and T2 are one
// or more derived types)
public static bool operator ==(BaseId left, BaseId right)
{
// Eventually calls left.Equals(object right), which is
// overridden in the derived class
return Equals(left, right);
}
public static bool operator !=(BaseId left, BaseId right)
{
// Eventually calls left.Equals(object right), which is
// overridden in the derived class
return !Equals(left, right);
}
}
我的目标是在基类中保留尽可能多的实现,以便派生类很小,主要/完全由样板代码组成。
请注意,此派生类型没有定义自己的其他状态。 它的目的仅仅是创建一个强类型。
public sealed class DerivedId : BaseId, IEquatable<DerivedId>
{
protected override string TypeName { get { return "DerivedId"; } }
public DerivedId(string id) : base(id) {}
public bool Equals(DerivedId other)
{
// Method signature ensures same (or derived) types, so
// defer to BaseId.Equals(object) override
return base.Equals(other);
}
// Override this so that unrelated derived types (e.g. BarId)
// NEVER match, regardless of underlying Id string value
public override bool Equals(object obj)
{
// Pass obj or null for non-DerivedId types to our
// Equals(DerivedId) override
return Equals(obj as DerivedId);
}
// warning CS0659 (see comment #2 below)
//public override int GetHashCode() { return base.GetHashCode(); }
}
不覆盖 BaseId 中的 Object.Equals(object o) 会生成编译警告:
warning CS0660: 'BaseId' defines operator == or operator != but does not override Object.Equals(object o)
但是如果我实现了 BaseId.Equals(object o),它只会调用 Object.Equals(object o) 中的基类实现。 无论如何,我不知道这将如何被调用; 它总是在派生类中被覆盖,并且那里的实现不会调用这个实现。
未覆盖 DerivedId 中的 BaseId.GetHashCode() 会生成编译警告:
warning CS0659: 'DerivedId' overrides Object.Equals(object o) but does not override Object.GetHashCode()
这个派生类没有额外的状态,所以我在 DerivedId.GetHashCode() 的实现中没有什么可做的,除了在 BaseId.GetHashCode() 中调用基类实现。
我可以取消编译器警告,或者只是实现方法并让它们调用基类实现,但我想确保我没有遗漏一些东西。
我这样做的方式有什么奇怪的吗,或者这只是为了抑制对其他正确代码的警告而必须做的事情之一?
这些是警告而不是错误的原因是代码仍然可以工作(可能),但它可能会做你不期望的事情。 警告是一个很大的红旗,上面写着:“嘿!你可能在这里做了坏事。你可能想再看看它。”
事实证明,警告是正确的。
在这种特殊情况下,某些代码可能会在您的BaseId
对象之一上调用Object.Equals(object)
。 例如,有人可以这样写:
bool CompareThings(BaseId thing, object other)
{
return thing.Equals(other);
}
编译器将生成对Object.Equals(object)
的调用,因为您的BaseId
类型不会覆盖它。 该方法将进行默认比较,这与Object.ReferenceEquals(object)
相同。 所以你有两种不同的Equals
含义。 在检查被比较的对象确实是BaseId
类型后,您需要覆盖Object.Equals(object)
并让它调用Equals(BaseId)
。
在第二种情况下,您是对的:可能不需要覆盖GetHashCode
,因为该对象没有定义任何新字段或做任何改变 Equals 含义的事情。 但是编译器不知道。 当然,它知道您没有添加任何字段,但您确实覆盖了Equals
,这意味着您可能改变了相等的含义。 如果你改变了相等的含义,那么你很可能改变(或应该改变)哈希码的计算方式。
在设计新类型时,没有正确处理相等性是一个非常常见的错误原因。 编译器在这方面过于谨慎是一件好事。
类拥有多个可覆盖(虚拟或抽象)的Equals
方法通常是不好的。 要么派生类覆盖Equals(object)
本身,要么将Equals(object)
(可能还有GetHashCode()
)的密封基础实现链接到抽象或虚拟Equals(BaseId)
(可能还有GetDerivedHashCode()
)。 目前尚不清楚您的目标究竟是什么,但我建议如果 ID 和类型都匹配时总是应该相等,而 ID 或类型不匹配时则不相等,则您的基本类型不需要包含任何相等性检查; 只需让基本相等检查测试类型是否匹配(可能使用GetType()
而不是TypeName
)。
我应该提一下,顺便说一句,我通常不喜欢重载==
和!=
类,除非它们应该从根本上表现为值。 在 C# 中, ==
运算符可以调用重载的相等性检查运算符或测试引用相等性; 比较以下效果:
static bool IsEqual1<T>(T thing1, thing2) where T:class
{
return thing1 == thing2;
}
static bool IsEqual2<T>(T thing1, thing2) where T:BaseId
{
return thing1 == thing2;
}
即使T
重载了相等检查运算符,上面的第一种方法也将执行引用相等性测试。 在第二个中,它将使用BaseId
的重载。 从视觉BaseId
,并不完全清楚BaseId
约束是否应该具有这样的效果,但确实如此。 在 vb.net 中,不会有任何混淆,因为 vb.net 不允许在IsEqual1
可重载的相等测试运算符; 如果需要在该方法中(或在第二种情况下)进行引用相等测试,则代码必须使用Is
运算符。 但是,由于 C# 使用相同的标记作为引用相等性测试和可重载相等性测试,因此==
标记的绑定并不总是很明显。
未覆盖
BaseId.GetHashCode()
DerivedId
生成编译警告:
在注释掉GetHashCode()
方法的情况下运行以下代码,然后再次不注释掉它,您将看到当没有GetHashCode
的实现时,该set
包含Person
两个实例,但是当您添加GetHashCode
的实现时,该set
仅包含一个实例,说明某些操作/类使用GetHashCode
进行比较。
class Program
{
static void Main(string[] args)
{
Person p1 = new Person() { FirstName="Joe", LastName = "Smith"};
Person p2 = new Person() { FirstName="Joe", LastName ="Smith"};
ISet<Person> set = new HashSet<Person>();
set.Add(p1);
set.Add(p2);
foreach (var item in set)
{
Console.WriteLine(item.FirstName);
}
}
}
class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public override bool Equals(object obj)
{
if (obj == null) return false;
var that = obj as Person;
if (that == null) return false;
return
FirstName == that.FirstName &&
LastName == that.LastName;
}
public override int GetHashCode() //run the code with and without this method
{
int hashCode = 1938039292;
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(FirstName);
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(LastName);
return hashCode;
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.