[英]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
方法,您必须检查:
this
objectthis
object 的类型相同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: 如果那对您还不够好,您可以决定:
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? 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
. 如评论中所述,如果您确定
this
与other
相等,那么使用上述方法就无法停止评估(短路)。 (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
,则可以进行短路。)例如,如果this
与other
具有引用相等性,则可以短路,因为毫无疑问一个对象等于其自身。
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
短路true
和false
,我们可以添加一个新的equals方法,该方法返回bool?
to represent three states: 代表三种状态:
true
: this
is definitely equal to other
without any need to check derived classes' properties. true
: this
绝对等于other
类,无需检查派生类的属性。 (Short-circuit.) false
: this
is definitely not equal to other
without any need to check derived classes' properties. false
: this
绝对不等于other
无需检查派生类的属性。 (Short-circuit.) null
: this
might or might not be equal to other
, depending on derived classes' properties. null
: this
可能或可能不等于other
,这取决于派生类的属性。 ( Do not short-circuit.) Since this doesn't match the bool
of Equals
, you need to define Equals
in terms of BaseEquals
. 由于这与
Equals
的bool
不匹配,因此需要根据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
,如果答案已经确定( true
或false
),则选择短路,否则,请找出当前类是否证明不平等。 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
. 因此,在
Equals
, null
表示继承层次结构中没有类可以确定不平等,因此两个对象相等,并且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.