![](/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.