How can Contains returns false but GetHashCode() returns the same number, and Equals returns true?
Asked Answered
C

4

5

I have an entity class like this (with lots of stuff missing):

class Parent
{
    private readonly Iesi.Collections.Generic.ISet<Child> children =
        new Iesi.Collections.Generic.HashedSet<Child>();

    public virtual void AddChild(Child child)
    {
        if (!this.children.Contains(child))
        {
            this.children.Add(child);
            child.Parent = this;
        }
    }

    public virtual void RemoveChild(Child child)
    {
        if (this.children.Contains(child))
        {
            child.Parent = null;
            this.children.Remove(child);
        }
    }
}

However, when I attempt to remove a child, the if statement evaluates to false. So, I put a breakpoint on the if statement, and evaluated certain expressions:

this.children.Contains(child) => false
this.children.ToList()[0].Equals(child) => true
this.children.ToList()[0].GetHashCode() => 1095838920
child.GetHashCode() => 1095838920

My understanding is that if GetHashCode returns identical values, it then checks Equals. Why is Contains returning false?


Both of my Parent and Child entities inherit from a common Entity base class, which is a non-generic version of the generic entity base class from page 25 of the NHibernate 3.0 Cookbook. Here is my base class:

public class Entity : IEntity
{
    public virtual Guid Id { get; private set; }

    public override bool Equals(object obj)
    {
        return Equals(obj as Entity);
    }

    private static bool isTransient(Entity obj)
    {
        return obj != null &&
            Equals(obj.Id, Guid.Empty);
    }

    private Type getUnproxiedType()
    {
        return GetType();
    }

    public virtual bool Equals(Entity other)
    {
        if (other == null)
            return false;

        if (ReferenceEquals(this, other))
            return true;

        if (!isTransient(this) &&
            !isTransient(other) &&
            Equals(Id, other.Id))
        {
            var otherType = other.getUnproxiedType();
            var thisType = getUnproxiedType();
            return thisType.IsAssignableFrom(otherType) ||
                otherType.IsAssignableFrom(thisType);
        }

        return false;
    }

    public override int GetHashCode()
    {
        if (Equals(Id, Guid.Empty))
            return base.GetHashCode();

        return Id.GetHashCode();
    }

}

After further investigation, I feel something like this is happening:

  1. Calling parent.AddChild(child)
  2. Saving to the database, which caused child.Id to be generated
  3. Calling parent.RemoveChild(child)

...and as discussed below, this was changing GetHashCode().

This was the result of a bug in my program - I was supposed to reload parent between steps 2 and 3.

Still, I think there's something more fundamentally wrong.

Cloaca answered 15/11, 2011 at 20:2 Comment(1)
Out of curiosity, could you test it with private Iesi.Collections.Generic.HashedSet<Child> children = new Iesi.Collections.Generic.HashedSet<Child>();Dimaggio
C
4

To get this to work, I had to change my Entity class' GetHashCode method to lazy-evaluate the hash code, but once computed, cache the result and never let it change. Here is my new implementation of GetHashCode:

    private int? requestedHashCode;

    public override int GetHashCode()
    {
        if (!requestedHashCode.HasValue)
        {
            requestedHashCode = isTransient(this) 
                ? base.GetHashCode() 
                : this.Id.GetHashCode();
        }
        return requestedHashCode.Value;
    }

For a better implementation of a base entity class, see AbstractEntity.

Cloaca answered 15/11, 2011 at 21:16 Comment(0)
K
3

I can't think of any other way that this could happen - Iesi.Collections.Generic.HashedSet must contain its own Contains that behaves differently than we expect.

Kidd answered 15/11, 2011 at 20:7 Comment(2)
Looking into it with ReSharper, it's just a wrapper around a standard Dictionary<T,object> and Contains calls ContainsKey.Cloaca
see this post about Dictionary.ContainsKeyKidd
O
3

Does Child both override object.Equals(object) and implement IEquatable<Child>? It's possible that the equality that the collection is doing isn't the same as the Equals method you're calling on the 2nd line of your code sample.

Op answered 15/11, 2011 at 20:15 Comment(0)
C
2

This could happen if Equals(Child) is implemented differently to the override of Equals(object). It would really depend on what Child looked like.

It can also happen due to the effect that Henk mention - is Parent part of the calculation of the hash code and equality, for example? If so, setting Parent to null will probably change the hash code of the child to be one other than the hash code which is recorded in the HashSet. It's not a problem if Parent isn't part of the equality/hash calculation, although it's still somewhat odd to put a mutable type into a hash set or use it as the key in a hash table.

Cribb answered 15/11, 2011 at 20:14 Comment(1)
Good points. Both derive from a common Entity base class (recommended from all NHibernate literature). I've posted the base class above.Cloaca

© 2022 - 2024 — McMap. All rights reserved.