Equals / GetHashCode在没有状态的派生类中覆盖警告

时间:2013-07-23 23:26:31

标签: c# .net override equals gethashcode

我为流经系统的各种字符串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(对象o)生成编译警告:

    warning CS0660: 'BaseId' defines operator == or operator != but does not override Object.Equals(object o)

    但是如果我实现BaseId.Equals(对象o),那么它只是调用Object.Equals(object o)中的基类实现。我不知道如何调用它会被调用;它总是在派生类中被覆盖,并且那里的实现并没有调用这个实现。

  2. 未覆盖DerivedId中的BaseId.GetHashCode()会生成编译警告:

    warning CS0659: 'DerivedId' overrides Object.Equals(object o) but does not override Object.GetHashCode()

    这个派生类没有其他状态,因此除了在BaseId.GetHashCode()中调用基类实现外,我没有在DerivedId.GetHashCode()的实现中做任何事情。

  3. 我可以抑制编译器警告或只是实现方法并让它们调用基类实现,但我想确保我没有遗漏某些东西。

    我这样做的方式是否有些奇怪,或者这只是为了抑制其他正确代码警告而必须采取的措施之一?

3 个答案:

答案 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)

解决问题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;
    }
}