Reputation: 21
I am using identifier classes in my project to keep track of certain variables. During writing of unit tests for these classes I noticed that the set actions did not behave as expected.
A minimal example to reproduce this problem is the following:
public void HashDerivedTest()
{
var setA = new HashSet<DerivedTestNumber>
{
new DerivedTestNumber(1),
new DerivedTestNumber(2),
new DerivedTestNumber(3)
}.Cast<TestNumberId>().ToHashSet();
var setB = new HashSet<TestNumberId> { new TestNumberId(1), new TestNumberId(3), new TestNumberId(5) };
var setAHashes = setA.Select(x => x.GetHashCode()).ToList();
var setBHashes = setB.Select(x => x.GetHashCode()).ToList();
var intersect = setA.Intersect(setB).ToSet();
Assert.AreEqual(2, intersect.Count); // This will return 0.
Assert.IsTrue(intersect.Contains(new TestNumberId(1))); // This will return false.
Assert.IsTrue(intersect.Contains(new TestNumberId(3))); // this will return false.
}
private abstract class BaseNumberClass<TVal>
where TVal : IEquatable<TVal>, IComparable<TVal>
{
public BaseNumberClass(TVal value) => this.Value = value;
public TVal Value { get; }
public override int GetHashCode() => this.Value.GetHashCode();
}
private class TestNumberId : BaseNumberClass<long>
{
public TestNumberId(long id)
: base(id)
{
}
}
private class DerivedTestNumber : TestNumberId
{
public DerivedTestNumber(long id)
: base(id)
{
}
}
I am fully aware that Hashsets do not implement covariance, but IEnumerables should. Thus casting in this way should create a copy of the original list with the new list having the same type.
Why does this not behave like this? And is there somehow to make it behave as expected?
Upvotes: 0
Views: 71
Reputation: 21
For future reference:
It seems this has to do with behaviour of the hashset comparison which will check on equality if the hashcodes of two objects are equal, but they do not have the same class.
The method for solving this is to implement the equals so it checks for inheritance.
This can be done by adding the following equality test: This issue can be solved by adding the following equality to the base class:
public override bool Equals(object other)
{
if (ReferenceEquals(this, other))
return true;
if (other is null)
return false;
// This tests for inheritance from both sides of the comparison.
// Direct comparison of the subclass is not allowed as this would always be this class.
return (other.GetType() == this.GetType() ||
other.GetType().IsSubclassOf(this.GetType()) ||
this.GetType().IsSubclassOf(other.GetType())) &&
this.Value.Equals(((BaseNumberClass<TVal>)other).Value);
}
Upvotes: -1