2

我为流经我们系统的各种字符串 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 中调用基类实现()。

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

我这样做的方式有什么奇怪的吗,或者这只是你必须做的事情之一,以抑制对其他正确代码的警告?

4

3 回答 3

3

这些是警告而不是错误的原因是代码仍然可以工作(可能),但它可能会做你不期望的事情。警告是一个大红旗,上面写着:“嘿!你可能在这里做错了。你可能想再看看它。”

事实证明,警告是正确的。

在这种特殊情况下,某些代码可能会调用Object.Equals(object)您的某个BaseId对象。例如,有人可以写:

bool CompareThings(BaseId thing, object other)
{
    return thing.Equals(other);
}

编译器将生成一个调用,Object.Equals(object)因为您的BaseId类型不会覆盖它。该方法将进行默认比较,与Object.ReferenceEquals(object). 所以你有两种不同的含义Equals。在检查被比较的对象确实是 type 之后,您需要覆盖Object.Equals(object)并调用它。Equals(BaseId)BaseId

在第二种情况下,你是对的:可能不需要覆盖GetHashCode,因为对象没有定义任何新字段或做任何改变 Equals 含义的事情。但是编译器不知道这一点。当然,它知道你没有添加任何字段,但你确实覆盖了Equals,这意味着你可能改变了相等的含义。如果你改变了相等的含义,那么你很可能改变(或应该改变)哈希码的计算方式。

在设计新类型时,不正确处理相等是一个非常常见的错误原因。编译器在这方面过于谨慎是件好事。

于 2013-07-24T00:04:31.220 回答
1

类具有多个可重写(虚拟或抽象)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,但确实如此。在 vb.net 中,不会有混淆,因为 vb.net 不允许在 ; 中使用可重载的相等测试运算符IsEqual1。如果该方法(或第二种方法)需要引用相等测试,则代码必须使用Is运算符。然而,由于 C# 使用相同的标记作为引用相等测试和可重载相等测试,因此==标记的绑定并不总是显而易见的。

于 2013-07-24T16:19:33.740 回答
1

解决问题中的问题 #2:

不覆盖BaseId.GetHashCode()DerivedId生成编译警告:

运行以下代码,将GetHashCode()方法注释掉,然后再次不注释掉它,你会看到当没有包含的两个实例的GetHashCode实现时,但是当你添加包含一个实例的实现时,证明了一些操作/classes用于比较。setPersonGetHashCodesetGetHashCode


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;
    }
}
于 2020-08-15T02:32:59.280 回答