简体   繁体   English

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

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

Background背景

I have a base class which holds an integer ID that is used for ORM (Microsoft Entity Framework).我有一个基数 class,它包含一个用于 ORM(Microsoft 实体框架)的 integer ID。 There are about 25 classes derived from this, and the inheritance hierarchy is up to 4 classes deep.从中派生出大约 25 个类,inheritance 层次结构最多有 4 个类深。

Requirement要求

I need to be able to test if an object in this hierarchy is equal to another object. To be equal it is necessary but not sufficient for the IDs to be the same.我需要能够测试此层次结构中的 object 是否等于另一个 object。要相等,ID 必须相同但还不够。 For example, if two Person objects have different IDs then they are not equal, but if they have the same ID then they may or may not be equal.例如,如果两个Person对象具有不同的 ID,则它们不相等,但如果它们具有相同的 ID,则它们可能相等也可能不相等。

Algorithm算法

In order to implement the C# Equals method you have to check that:为了实施 C# Equals方法,您必须检查:

  • The Supplied object is not null.提供的 object 不是 null。
  • It must be of the same type as this object它必须与this object 的类型相同
  • The ID s must match ID必须匹配

In addition to this, all other attributes must be compared, except in the special case where the two objects are identical.除此之外,必须比较所有其他属性,两个对象相同的特殊情况除外。

Implementation执行

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

Comments注释

The feature of this solution that I dislike the most is the explicit conversions.我最不喜欢这个解决方案的特点是显式转换。 Is there an alternative solution, which avoids having to repeat all the common logic (checking for null, type etc) in each class?是否有替代解决方案,避免在每个 class 中重复所有通用逻辑(检查 null、类型等)?

This is pretty easy using generics: 使用泛型非常简单:

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

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

This works fine for one level of inheritance, or when all the levels are abstract except for the last one. 这对于继承的一个级别或在所有级别都是abstract的(最后一个除外)时都很abstract

If that's not good enough for you, you have a decision to make: 如果那对您还不够好,您可以决定:

  • If it's not all that common, it might be just fine to keep the few exceptions with manual casts. 如果不是那么普遍,最好通过手动强制转换保留一些例外。
  • If it is common, you're out of luck. 如果它常见的,你的运气了。 Making Person generic works, but it kind of defeats the purpose - it requires you to specify the concrete Person -derived type whenever you need to use Person . 使Person通用是可行的,但这有损于目的-它要求您在需要使用Person时指定具体的Person派生类型。 This can be handled by having an interface IPerson that's not generic. 这可以通过使用通用接口IPerson来解决。 Of course, in effect, this still means that Person is abstract - you have no way of constructing a non-concrete version of Person . 当然,实际上,这仍然意味着Person是抽象的-您无法构造Person的非具体版本。 Why wouldn't it be abstract, in fact? 为什么不会是抽象的,其实? Can you have a Person that isn't one of the derived types of Person ? 你可以有一个Person是不是派生类型中的一种Person That sounds like a bad idea. 听起来像个坏主意。

It seems simpler if instead of using abstract , you just keep overriding the Equals method for subclasses. 如果不使用abstract ,而只是继续为子类覆盖Equals方法,则似乎更简单。 Then you can extend like this: 然后,您可以像这样扩展:

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

You have to cast to Person , but this works with relatively few lines of code and with long hierarchies without any worries. 您必须强制转换为Person ,但这可以用很少的代码行和较长的层次结构实现,而无需担心。 (Because you already checked for the runtime types being the same in the very root of the hierarchy, you don't even have to do a as Person with a null check.) (由于您已经在层次结构的最根部检查了运行时类型是否相同,因此您甚至不必使用null检查来执行as Person 。)


As mentioned in the comments, with the above approach you can't stop evaluating (short-circuit) if you know for certain this is equal to other . 如评论中所述,如果您确定thisother相等,那么使用上述方法就无法停止评估(短路)。 (Although you do short-circuit if you know for sure this is not equal to other .) For example, if this has reference equality with other , you can short-circuit because there's no doubt that an object is equal to itself. (尽管如果您确定this 等于other ,则可以进行短路。)例如,如果thisother具有引用相等性,则可以短路,因为毫无疑问一个对象等于其自身。

Being able to return early would mean that you can skip a lot of checks. 能够提早归还意味着您可以跳过很多支票。 This is useful if the checks are expensive. 如果支票价格昂贵,这很有用。

To allow Equals to short-circuit true as well as false , we can add a new equality method that returns bool? 为了使Equals短路truefalse ,我们可以添加一个新的equals方法,该方法返回bool? to represent three states: 代表三种状态:

  • true : this is definitely equal to other without any need to check derived classes' properties. truethis绝对等于other类,无需检查派生类的属性。 (Short-circuit.) (短路)。
  • false : this is definitely not equal to other without any need to check derived classes' properties. falsethis绝对不等于other无需检查派生类的属性。 (Short-circuit.) (短路)。
  • null : this might or might not be equal to other , depending on derived classes' properties. nullthis可能或可能不等于other ,这取决于派生类的属性。 ( Do not short-circuit.) 请勿短路。)

Since this doesn't match the bool of Equals , you need to define Equals in terms of BaseEquals . 由于这与Equalsbool不匹配,因此需要根据BaseEquals定义Equals Each derived class checks its base class' BaseEquals and chooses to short circuit if an answer is already definite ( true or false ) and if not, find out if the current class proves inequality. 每个派生类都会检查其基类的BaseEquals ,如果答案已经确定( truefalse ),则选择短路,否则,请找出当前类是否证明不平等。 In Equals , then, a null means that no class in the inheritance hierarchy could determine inequality, so the two objects are equal and Equals should return true . 因此,在Equalsnull表示继承层次结构中没有类可以确定不平等,因此两个对象相等,并且Equals应该返回true Here's an implementation that will hopefully explain this better: 这是一个希望可以更好地解释这一点的实现:

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

Well here is a variant of @31eee384 one's.好吧,这是 @31eee384 的一个变体。

I don't use it's trinary abstract method.我不使用它的三元抽象方法。 I suppose that if base.Equals() return true , I still need to perform the derived Equals checks too.我想如果base.Equals()返回true ,我仍然需要执行派生的 Equals 检查。

The drawback though is that you renounce to have the Reference Equality in base.Equals to propagate this "short-circuit" in derived classes Equals method.但缺点是您放弃在 base.Equals 中使用引用相等性以在派生类 Equals 方法中传播此“短路”。

Maybe there exists something in C# to "force to stop" the overriding somehow and "hard return true" when the reference equality is true without continuing the overridden derived Equals calls.也许在 C# 中存在某些东西以某种方式“强制停止”覆盖,并在引用相等性为真时“硬返回真”而不继续被覆盖的派生 Equals 调用。

Also do note that following 31eee384 answer, we give up the template method pattern used by OP.另请注意,在 31eee384 答案之后,我们放弃了 OP 使用的模板方法模式。 Using this pattern again actually goes back to OP's implementation.再次使用这种模式实际上可以追溯到 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();
    }
}

Here is my fiddle link .这是我的小提琴链接

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 有一个带有 CRUD 实现的基本实体类和一个可以由实现它的类更改的主键 ID - Have a base entity class with CRUD implementation and a primary key ID that can be changed by the classes implementing it 如何创建一个包含类的类? - How to make a class that holds classes? 如何获得 System.Collections.Generic.List<fieldinfo> 它包含 class 层次结构中类型 T 的 object 到 Object 的所有 FieldInfo?</fieldinfo> - How to get a System.Collections.Generic.List<FieldInfo> which holds all FieldInfo's of an object of a Type T up to the Object in the class hierarchy? 如何将实体的主键与基数 class 分开? - How to separate entity's primary key from base class? 是否有可能已经从某个东西继承或知道第三个公共类的2个基类? - Is it possible to have 2 Base class's which already inherit from something inherit or know of a third common class? TPT基类主键标识约定 - TPT Base Class Primary Key Identity convention 您如何使用反射获得一个类及其基类(在层次结构中)的所有属性? (C#) - How do you get the all properties of a class and its base classes (up the hierarchy) with Reflection? (C#) 扁平化用于映射和模式导出的类层次结构(只读基类!) - Flatten class hierarchy for mapping and schema export (readonly base classes!) 如何隐藏从基类派生的类中的方法,该方法是基类中接口实现的一部分? - How to hide a method, which is part of an interface implementation within a base class, from classes that derive from the base class? 如何将字符串传递给基本层次结构头类 - How to pass a string to base hierarchy header class
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM