我为流经系统的各种字符串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(对象o)生成编译警告:
warning CS0660: 'BaseId' defines operator == or operator != but does not override Object.Equals(object o)
但是如果我实现BaseId.Equals(对象o),那么它只是调用Object.Equals(object o)中的基类实现。我不知道如何调用它会被调用;它总是在派生类中被覆盖,并且那里的实现并没有调用这个实现。
未覆盖DerivedId中的BaseId.GetHashCode()会生成编译警告:
warning CS0659: 'DerivedId' overrides Object.Equals(object o) but does not override Object.GetHashCode()
这个派生类没有其他状态,因此除了在BaseId.GetHashCode()中调用基类实现外,我没有在DerivedId.GetHashCode()的实现中做任何事情。
我可以抑制编译器警告或只是实现方法并让它们调用基类实现,但我想确保我没有遗漏某些东西。
我这样做的方式是否有些奇怪,或者这只是为了抑制其他正确代码警告而必须采取的措施之一?
答案 0 :(得分:2)
这些是警告而不是错误的原因是代码仍然可以工作(可能),但它可能会做你不期望的事情。警告是一个大红旗,上面写着:“嘿!你可能会在这里做些坏事。你可能想再看看它。”
事实证明,警告是正确的。
在这种特殊情况下,某些代码可能会在您的Object.Equals(object)
个对象上调用BaseId
。例如,有人可以写:
bool CompareThings(BaseId thing, object other)
{
return thing.Equals(other);
}
编译器将生成对Object.Equals(object)
的调用,因为您的BaseId
类型不会覆盖它。该方法将执行默认比较,这与Object.ReferenceEquals(object)
相同。所以你有Equals
的两个不同含义。在检查被比较的对象确实是Object.Equals(object)
类型之后,您需要覆盖Equals(BaseId)
并让它调用BaseId
。
在第二种情况下,你是对的:可能不需要覆盖GetHashCode
,因为对象没有定义任何新字段或做任何改变Equals含义的事情。但是编译器不知道这一点。当然,它知道你没有添加任何字段,但你确实覆盖了Equals
,这意味着你可能改变了相等的含义。如果你改变了相等的含义,那么你很可能改变(或应该改变)哈希码的计算方式。
在设计新类型时,不正确处理相等性是导致错误的常见原因。编译器在这方面过于谨慎是一件好事。
答案 1 :(得分:1)
类通常不适合使用多个可覆盖(虚拟或抽象)Equals
方法。要么派生类本身覆盖Equals(object)
,要么将Equals(object)
(可能GetHashCode()
)的密封基本实现链接到抽象或虚拟Equals(BaseId)
(可能{{} 1}})。目前还不清楚你的目标究竟是什么,尽管我建议如果ID和类型都匹配则总是相等,如果ID或类型不匹配则不相等,你的基类型不需要包含任何相等的检查;只需要进行基本相等性检查测试类型是否匹配(可能使用GetDerivedHashCode()
而不是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
约束应该具有这样的效果并不完全清楚,但确实如此。在vb.net中,没有混淆,因为vb.net不允许BaseId
中的可重载的相等测试运算符;如果在该方法中需要引用相等性测试(或者在第二种方法中),则代码必须使用IsEqual1
运算符。但是,由于C#使用与引用相等性测试和可重载相等性测试相同的令牌,因此Is
令牌的绑定并不总是显而易见。
答案 2 :(得分:0)
不覆盖
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;
}
}