我在使用带有重写的 Equals 方法的对象中使用 .NET 集合(List 和 HashSet)时遇到问题。
上下文(代码如下):
- 我
- 有一个基类,我通过调用另一个方法(Equals(EntityBase<>)或使用完全不同的名称来实现 Equals(object),其中我比较这两个对象的 Id。
- 我有一个从基类派生的具体类。
- 我创建了一个集合(基本上是另一个对象的导航属性)。
- 在测试中,我在NSubstitute的帮助下创建了对象代理(内部使用Castle),但在现实世界中,它可以是EntityFramework或NHibernate代理。
- 将对象放入集合后,集合找不到它或具有相同 id 的另一个对象。
- 我无法从集合中删除对象。
作为一种解决方法,我不得不在 Equals 方法中复制 Equals(EntityBase<>) 的代码,这解决了这个问题。
环境: VS2013 SP4, NUnit, NSubstitute, .NET Framework 4.5.1.
我想知道当从 Equals 调用另一个方法时,这种行为的原因是什么。
代码:
[TestFixture]
public class ObjectEqualsTests
{
[Test]
public void CollectionAddRemoveEntityTest()
{
const int id = 12345;
var list = new List<MyObject>();
var firstObject = Substitute.For<MyObject>();
firstObject.Id.Returns(id);
list.Add(firstObject);
Assert.IsTrue(list.Contains(firstObject), "Cannot find the first object");
var secondObjectWithSameId = Substitute.For<MyObject>();
secondObjectWithSameId.Id.Returns(id);
Assert.IsTrue(list.Contains(secondObjectWithSameId), "Cannot find the second object");
list.Remove(secondObjectWithSameId);
Assert.AreEqual(0, list.Count, "Object was not removed from the list");
}
}
public class MyObject : EntityBase<int>
{
public virtual string SomeProperty { get; set; }
}
public interface IEntity<TId>
{
TId Id { get; }
}
public class EntityBase<TId> : IEntity<TId>, IEquatable<EntityBase<TId>>
{
public virtual TId Id { get; protected set; }
public override bool Equals(object obj)
{
var other = obj as EntityBase<TId>;
// if to remove next two lines, the test passes
return Equals(other); // the first implementation
return EqualsWithDifferentName(other); // first attempt to fix
// second attempt to fix by duplicating the code
if (other == null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
if (!IsTransient(this) && !IsTransient(other) && Equals(Id, other.Id))
{
var otherType = other.GetUnproxfiedType();
var thisType = GetUnproxfiedType();
return thisType.IsAssignableFrom(otherType) && otherType.IsAssignableFrom(thisType);
}
return false;
}
private Type GetUnproxfiedType()
{
return GetType();
}
private static bool IsTransient(EntityBase<TId> obj)
{
return obj != null && Equals(obj.Id, default(TId));
}
public virtual bool Equals(EntityBase<TId> other)
{
if (other == null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
if (!IsTransient(this) && !IsTransient(other) && Equals(Id, other.Id))
{
var otherType = other.GetUnproxfiedType();
var thisType = GetUnproxfiedType();
return thisType.IsAssignableFrom(otherType) && otherType.IsAssignableFrom(thisType);
}
return false;
}
public virtual bool EqualsWithDifferentName(EntityBase<TId> other)
{
if (other == null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
if (!IsTransient(this) && !IsTransient(other) && Equals(Id, other.Id))
{
var otherType = other.GetUnproxfiedType();
var thisType = GetUnproxfiedType();
return thisType.IsAssignableFrom(otherType) && otherType.IsAssignableFrom(thisType);
}
return false;
}
public override int GetHashCode()
{
return Equals(Id, default(TId)) ? base.GetHashCode() : Id.GetHashCode();
}
}
更新:与其使用 Substitute.For<>不如使用 Substitute.ForPartsOf<>。
使用 For<> 创建代理时,它将覆盖所有可以重写的方法。在这种情况下,Equals(EntityBase<>)具有空的实现,调试器无法单步执行此方法。
当对象与 ORM 一起使用时,从方法声明中删除 virtual 关键字的选项可能不起作用,ORM 要求每个方法和属性都使用 virtual(我的意思是 NHibernate)。
请检查一下
[TestFixture]
public class ObjectEqualsTests
{
[Test]
public void CollectionAddRemoveEntityTest()
{
const int id = 12345;
var list = new List<MyObject>();
var firstObject = Substitute.For<MyObject>();
firstObject.Id.Returns(id);
list.Add(firstObject);
Assert.IsTrue(list.Contains(firstObject), "Cannot find the first object");
var secondObjectWithSameId = Substitute.For<MyObject>();
secondObjectWithSameId.Id.Returns(id);
Assert.IsTrue(list.Contains(secondObjectWithSameId), "Cannot find the second object");
list.Remove(secondObjectWithSameId);
Assert.AreEqual(0, list.Count, "Object was not removed from the list");
}
}
public class MyObject : EntityBase<int>
{
public virtual string SomeProperty { get; set; }
}
public interface IEntity<TId>
{
TId Id { get; }
}
public class EntityBase<TId> : IEntity<TId>
{
public virtual TId Id { get; protected set; }
public override bool Equals(object other)
{
EntityBase<TId> otherEntity = other as EntityBase<TId>;
if ((object)otherEntity == null)
{
return false;
}
return Equals(otherEntity);
}
public bool Equals(EntityBase<TId> other)
{
if (other == null)
{
return false;
}
if (ReferenceEquals(this, other))
{
return true;
}
if (!IsTransient(this) && !IsTransient(other) && Equals(Id, other.Id))
{
var otherType = other.GetUnproxfiedType();
var thisType = GetUnproxfiedType();
return thisType.IsAssignableFrom(otherType) && otherType.IsAssignableFrom(thisType);
}
return false;
}
public override int GetHashCode()
{
return Equals(Id, default(TId)) ? base.GetHashCode() : Id.GetHashCode();
}
private Type GetUnproxfiedType()
{
return GetType();
}
private static bool IsTransient(EntityBase<TId> obj)
{
return obj != null && Equals(obj.Id, default(TId));
}
}
现在测试通过
漏洞点是省略virtual
public bool Equals(EntityBase<TId> other)
语句。可能NSubstitute代理有自己的方法实现。