具有对象代理对象和等于的 C# 集合



我在使用带有重写的 Equals 方法的对象中使用 .NET 集合(List 和 HashSet)时遇到问题。

上下文(代码如下):

  1. 有一个基类,我通过调用另一个方法(Equals(EntityBase<>)或使用完全不同的名称来实现 Equals(object),其中我比较这两个对象的 Id。
  2. 我有一个从基类派生的具体类。
  3. 我创建了一个集合(基本上是另一个对象的导航属性)。
  4. 在测试中,我在NSubstitute的帮助下创建了对象代理(内部使用Castle),但在现实世界中,它可以是EntityFramework或NHibernate代理。
  5. 将对象放入集合后,集合找不到它或具有相同 id 的另一个对象。
  6. 我无法从集合中删除对象。

作为一种解决方法,我不得不在 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代理有自己的方法实现。

最新更新