繁体   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