如何优雅地检查 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
方法,您必须检查:
- 提供的对象不为空。
- 它必须与
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;
}
}
评论
我最不喜欢这个解决方案的特点是显式转换。 是否有替代解决方案,避免在每个 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
短路true
和false
,我们可以添加一个新的相等方法returns bool?
到代表三种状态:
true
:this
绝对等于 other
,无需检查派生的 classes 的属性。 (短路。)
false
:this
绝对不等于 other
,无需检查派生的 classes 的属性。 (短路。)
null
:this
可能等于也可能不等于 other
,具体取决于派生的 classes 的属性。 (不要短路。)
由于这与Equals
的bool
不匹配,您需要根据BaseEquals
定义Equals
。每个派生 class 检查其基数 class' BaseEquals
并在答案已经确定(true
或 false
)时选择短路,如果不是,则查找如果当前 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。
背景
我有一个基数 class,它包含一个用于 ORM (Microsoft Entity Framework) 的整数 ID。大约有 25 classes 派生自此,继承层次高达 4 classes 深。
要求
我需要能够测试此层次结构中的对象是否等于另一个对象。要相等,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;
}
}
评论
我最不喜欢这个解决方案的特点是显式转换。 是否有替代解决方案,避免在每个 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
短路true
和false
,我们可以添加一个新的相等方法returns bool?
到代表三种状态:
true
:this
绝对等于other
,无需检查派生的 classes 的属性。 (短路。)false
:this
绝对不等于other
,无需检查派生的 classes 的属性。 (短路。)null
:this
可能等于也可能不等于other
,具体取决于派生的 classes 的属性。 (不要短路。)
由于这与Equals
的bool
不匹配,您需要根据BaseEquals
定义Equals
。每个派生 class 检查其基数 class' BaseEquals
并在答案已经确定(true
或 false
)时选择短路,如果不是,则查找如果当前 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。