如何在具有包含主键的公共基类的类层次结构中优雅地检查相等性

本文关键字:层次结构 检查 基类 包含主 | 更新日期: 2023-09-27 18:01:31

Background

我有一个基类,其中包含一个用于ORM(Microsoft实体框架(的整数ID。 从中派生了大约 25 个类,继承层次结构最多有 4 个类。

要求

我需要能够测试此层次结构中的一个对象是否等于另一个对象。要相等,ID必须相同,但还不够。 例如,如果两个Person对象具有不同的 ID,则它们不相等,但如果它们具有相同的 ID,则它们可能相等,也可能不相等。

算法

若要实现 C# Equals 方法,必须检查:

  • 提供的对象不为空。
  • 它必须与对象类型相同this
  • ID必须匹配

除此之外,还必须比较所有其他属性,除非在两个对象相同的特殊情况下。

实现

    /// <summary>
    /// An object which is stored in the database
    /// </summary>
    public abstract class DatabaseEntity
    {
        /// <summary>
        /// The unique identifier; if zero (0) then the ID is not assigned
        /// </summary>
        public int ID { get; set; }
        public override bool Equals(object obj)
        {
            if (obj == null)
            {
                return false;
            }
            if (ReferenceEquals(obj, this))
            {
                return true;
            }
            if (obj.GetType() != GetType())
            {
                return false;
            }
            DatabaseEntity databaseEntity = (DatabaseEntity)obj;
            if (ID != databaseEntity.ID)
            {
                return false;
            }
            return EqualsIgnoringID(databaseEntity);
        }
        public override int GetHashCode()
        {
            return ID;
        }
        /// <summary>
        /// Check if this object is equal to the supplied one, disregarding the IDs
        /// </summary>
        /// <param name="databaseEntity">another object, which should be of the same type as this one</param>
        /// <returns>true if they are equal (disregarding the ID)</returns>
        protected abstract bool EqualsIgnoringID(DatabaseEntity databaseEntity);
    }
    public class Person : DatabaseEntity
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public override bool EqualsIgnoringID(DatabaseEntity databaseEntity)
        {
            Person person = (Person)databaseEntity;
            return person.FirstName == FirstName && person.LastName == LastName;
        }
    }
    public class User: Person
    {
        public string Password { get; set; }
        public override bool EqualsIgnoringID(DatabaseEntity databaseEntity)
        {
            User user = (User)databaseEntity;
            return user.Password == Password;
        }
    }

评论

我最不喜欢的这个解决方案的功能是显式转换。是否有替代解决方案,可以避免在每个类中重复所有通用逻辑(检查 null、类型等(?

如何在具有包含主键的公共基类的类层次结构中优雅地检查相等性

如果不使用 abstract ,您只是继续覆盖子类的 Equals 方法,这似乎更简单。然后你可以像这样扩展:

public class Person : DatabaseEntity
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public override bool Equals(object other)
    {
        if (!base.Equals(other))
            return false;
        Person person = (Person)other;
        return person.FirstName == FirstName && person.LastName == LastName;
    }
}

你必须投射到Person,但这适用于相对较少的代码行和长层次结构,没有任何后顾之忧。(因为您已经检查了层次结构根目录中的运行时类型是否相同,所以您甚至不必使用 null 检查执行as Person


如评论中所述,使用上述方法,如果您知道某些this等于other,则无法停止评估(短路(。(虽然如果你确定this不等于other,你会短路。例如,如果thisother具有引用相等性,则可以短路,因为毫无疑问,对象与自身相等。

能够提前返回意味着您可以跳过很多检查。如果检查费用昂贵,这将非常有用。

为了允许Equals短路true以及false,我们可以添加一个新的相等方法,该方法返回bool?来表示三种状态:

  • truethis绝对等于other,无需检查派生类的属性。(短路。
  • falsethis绝对不等于other,而无需检查派生类的属性。(短路。
  • nullthis可能等于也可能不等于other,这取决于派生类的属性。(请勿短路。

由于这与Equalsbool不匹配,因此您需要根据BaseEquals来定义Equals。每个派生类检查其基类的BaseEquals,如果答案已经确定(truefalse(,则选择短路,如果不是,则找出当前类是否证明不等式。因此,在Equals中,null意味着继承层次结构中的任何类都不能确定不等式,因此两个对象相等,Equals应返回true。这是一个有望更好地解释这一点的实现:

public class DatabaseEntity
{
    public int ID { get; set; }
    public override bool Equals(object other)
    {
        // Turn a null answer into true: if the most derived class has not
        // eliminated the possibility of equality, this and other are equal.
        return BaseEquals(other) ?? true;
    }
    protected virtual bool? BaseEquals(object other)
    {
        if (other == null)
            return false;
        if (ReferenceEquals(this, other))
            return true;
        if (GetType() != other.GetType())
            return false;
        DatabaseEntity databaseEntity = (DatabaseEntity)other;
        if (ID != databaseEntity.ID)
            return false;
        return null;
    }
}
public class Person : DatabaseEntity
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    protected override bool? BaseEquals(object other)
    {
        bool? baseEquals = base.BaseEquals(other);
        if (baseEquals != null)
            return baseEquals;
        Person person = (Person)other;
        if (person.FirstName != FirstName || person.LastName != LastName)
            return false;
        return null;
    }
}

使用泛型很容易:

public abstract class Entity<T>
{
  protected abstract bool IsEqual(T other);
}
public class Person : Entity<Person>
{
  protected override bool IsEqual(Person other) { ... }
}

这适用于一个继承级别,或者当除最后一个级别外的所有级别都abstract时。

如果这对您来说还不够好,您需要做出决定:

  • 如果不是那么常见,那么保留手动转换的少数例外可能就可以了。
  • 如果它常见,你就不走运了。使Person泛型工作,但它有点违背目的 - 它要求您在需要使用 Person 时指定具体的Person派生类型。这可以通过使用通用的接口IPerson来处理。当然,实际上,这仍然意味着Person是抽象的——你没有办法构建一个非具体的Person版本。事实上,为什么它不是抽象的呢?你能有一个不是派生Person类型之一的Person吗?这听起来是个坏主意。
好吧,

这是@31eee384的变体。

我不使用它的三位一体抽象方法。我想如果base.Equals()返回true,我仍然需要执行派生的等于检查。

但缺点是你放弃了在基础中拥有参考平等。等于在派生类 Equals 方法中传播此"短路"。

也许 C# 中存在一些东西可以以某种方式"强制停止"重写,并在引用相等为真时"硬返回 true",而无需继续重写的派生 Equals 调用。

另请注意,在 31eee384 答案之后,我们放弃了 OP 使用的模板方法模式。 再次使用此模式实际上可以追溯到 OP 的实现。

public class Base : IEquatable<Base>
{
    public int ID {get; set;}
    public Base(int id)
    {ID = id;}
    public virtual bool Equals(Base other)
    {
        Console.WriteLine("Begin Base.Equals(Base other);");
    
        if (other == null) return false;
        if (ReferenceEquals(this, other)) return true;
        if (GetType() != other.GetType()) return false;
    
        return ID == other.ID;
    }
    public override bool Equals(object other)
    {
        return this.Equals(other as Base);
    }
    public override int GetHashCode()
    {
        unchecked
        {
            // Choose large primes to avoid hashing collisions
            const int HashingBase = (int) 2166136261;
            const int HashingMultiplier = 16777619;
            int hash = HashingBase;
            hash = (hash * HashingMultiplier) ^ (!Object.ReferenceEquals(null, ID) ? ID.GetHashCode() : 0);        
            return hash;
        }
    }
    public override string ToString()
    {
        return "A Base object with ["+ID+"] as ID";
    }
}
public class Derived : Base, IEquatable<Derived>
{
    public string Name {get; set;}
    public Derived(int id, string name) : base(id)
    {Name = name;}
    public bool Equals(Derived other)
    {   
        Console.WriteLine("Begin Derived.Equals(Derived other);");
    
        if (!base.Equals(other)) return false;
    
        return Name == other.Name;
    }
    public override bool Equals(object other)
    {
        return this.Equals(other as Derived);
    }   
    
    public override int GetHashCode()
    {
        unchecked
        {
            // Choose large primes to avoid hashing collisions
            const int HashingBase = (int) 2166136261;
            const int HashingMultiplier = 16777619;
            int hash = HashingBase;
            hash = (hash * HashingMultiplier) ^ base.GetHashCode();
            hash = (hash * HashingMultiplier) ^ (!Object.ReferenceEquals(null, Name) ? Name.GetHashCode() : 0);        
            return hash;
        }
    }
    public override string ToString()
    {
        return "A Derived object with '" + Name + "' as Name, and also " + base.ToString();
    }
}

这是我的小提琴链接。