簡體   English   中英

沒有狀態的派生類中的 Equals/GetHashCode 覆蓋警告

[英]Equals/GetHashCode override warning in derived class with no state

我為流經我們系統的各種字符串 ID 創建了一個強類型、不可變的包裝類

抽象 BaseId 類:

(為簡潔起見,省略了一些錯誤檢查和格式設置...)

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);
    }
}

我的目標是在基類中保留盡可能多的實現,以便派生類很小,主要/完全由樣板代碼組成。

示例具體 DerivedId 類:

請注意,此派生類型沒有定義自己的其他狀態。 它的目的僅僅是創建一個強類型。

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(); }
}

每個類都生成一個編譯器警告:

  1. 不覆蓋 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) 中的基類實現。 無論如何,我不知道這將如何被調用; 它總是在派生類中被覆蓋,並且那里的實現不會調用這個實現。

  2. 未覆蓋 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# 使用相同的標記作為引用相等性測試和可重載相等性測試,因此==標記的綁定並不總是很明顯。

解決問題中的問題#2:

未覆蓋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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM