如何在具有包含主键的公共基类的类层次结构中优雅地检查相等性
本文关键字:层次结构 检查 基类 包含主 | 更新日期: 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
,你会短路。例如,如果this
与other
具有引用相等性,则可以短路,因为毫无疑问,对象与自身相等。
能够提前返回意味着您可以跳过很多检查。如果检查费用昂贵,这将非常有用。
为了允许Equals
短路true
以及false
,我们可以添加一个新的相等方法,该方法返回bool?
来表示三种状态:
-
true
:this
绝对等于other
,无需检查派生类的属性。(短路。 -
false
:this
绝对不等于other
,而无需检查派生类的属性。(短路。 -
null
:this
可能等于也可能不等于other
,这取决于派生类的属性。(请勿短路。
由于这与Equals
的bool
不匹配,因此您需要根据BaseEquals
来定义Equals
。每个派生类检查其基类的BaseEquals
,如果答案已经确定(true
或false
(,则选择短路,如果不是,则找出当前类是否证明不等式。因此,在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();
}
}
这是我的小提琴链接。