如何优雅地检查 classes 的层次结构中的相等性,这些层次结构具有包含主键的公共基础 class?

How to elegantly check for equality in a hierarchy of classes which have a common base class that holds a primary key?

背景

我有一个基数 class,它包含一个用于 ORM (Microsoft Entity Framework) 的整数 ID。大约有 25 classes 派生自此,继承层次高达 4 classes 深。

要求

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

算法

为了实现 C# Equals 方法,您必须检查:

除此之外,所有其他属性都必须进行比较,两个对象相同的特殊情况除外。

实施

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

评论

我最不喜欢这个解决方案的特点是显式转换。 是否有替代解决方案,避免在每个 class?

中重复所有通用逻辑(检查 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 的派生类型之一吗?这听起来像是个坏主意。

如果不使用 abstract 似乎更简单,您只需继续覆盖 subclasses 的 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,但这适用于相对较少的代码行和较长的层次结构,无需担心。 (因为您已经在层次结构的最根部检查了运行时类型是否相同,所以您甚至不必执行 as Person 空检查。)


如评论中所述,使用上述方法,如果您确定 this 等于 other,则无法停止评估(短路)。 (尽管如果您确定 this 而不是 等于 other,您就会短路。)例如,如果 this 具有引用相等性使用other,你可以短路,因为毫无疑问,一个对象等于它自己。

能够提前 return 意味着您可以跳过很多检查。如果支票很贵,这很有用。

为了让Equals短路truefalse,我们可以添加一个新的相等方法returns bool?到代表三种状态:

  • truethis 绝对等于 other,无需检查派生的 classes 的属性。 (短路。)
  • falsethis 绝对不等于 other,无需检查派生的 classes 的属性。 (短路。)
  • nullthis 可能等于也可能不等于 other,具体取决于派生的 classes 的属性。 (不要短路。)

由于这与Equalsbool不匹配,您需要根据BaseEquals定义Equals。每个派生 class 检查其基数 class' BaseEquals 并在答案已经确定(truefalse)时选择短路,如果不是,则查找如果当前 class 证明不等式则输出。那么在Equals中,一个null表示继承层次中没有class可以判断不等式,所以两个对象是相等的,Equals应该returntrue。这是一个有望更好地解释这一点的实现:

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

这是@31eee384 的一个变体。

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

但缺点是您放弃在 base.Equals 中使用引用相等性以在派生的 类 等于方法中传播此“short-circuit”。

也许 C# 中存在一些东西可以以某种方式“强制停止”覆盖,并在引用相等性为真时“硬 return 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();
    }
}

这是我的fiddle link