I've an object that inherites from a base class that inherits from IEquatable<>. So far so good and it works for other objects that inherit the same base class. But I have on class "RoomType" where there seems to be a problem when I use the "Attrbiutes" property. Below you see the classes and a test where I expect other output that given.
I narrowed the problem down to something with the RoomType.GetHashCode(), when I comment "SafeHashCode(Attributes)" the expected result is returned.
Test:
private static void QuickTest()
{
RoomType[] rooms = new RoomType[] {
new RoomType {
Attributes = new [] { "a", "b,"c"},
},
new RoomType
{
Attributes = new [] { "a", "b","c"},
}
};
List<RoomType> result = rooms.Distinct().ToList();
//result contains 2 items, I was expecting 1
}
RoomType:
public class RoomType : EntityBase
{
public string OriginalRoomCode { get; set; }
public Enum.RoomType RoomCode { get; set; }
public IEnumerable<string> Attributes { get; set; }
public override bool Equals(object obj)
{
RoomType other = obj as RoomType;
if (other != null)
return Equals(other);
return false;
}
public override bool Equals(EntityBase obj)
{
RoomType y = (RoomType)obj;
return SafeEqual(OriginalRoomCode, y.OriginalRoomCode) &&
SafeEqual(RoomCode, y.RoomCode) &&
SafeEqual(Attributes,y.Attributes);
}
public override int GetHashCode()
{
unchecked
{
return SafeHashCode(OriginalRoomCode) ^
SafeHashCode(RoomCode) ^
SafeHashCode(Attributes);
}
}
public override object Clone()
{
return new RoomType
{
RoomCode = (Enum.RoomType)SafeClone(RoomCode),
OriginalRoomCode = (string)SafeClone(OriginalRoomCode),
Attributes = (IEnumerable<string>)SafeClone(Attributes)
};
}
}
EntityBase:
public abstract class EntityBase : IEquatable<EntityBase>, ICloneable
{
public bool SafeEqual<T>(T x, T y)
{
bool isXDefault = EqualityComparer<T>.Default.Equals(x, default(T));
bool isYDefault = EqualityComparer<T>.Default.Equals(y, default(T));
if (isXDefault && isYDefault)
return true;
if (isXDefault != isYDefault)
return false;
if (x is EntityBase)
return x.Equals(y);
IEnumerable<object> xEnumerable = x as IEnumerable<object>;
IEnumerable<object> yEnumerable = y as IEnumerable<object>;
if (xEnumerable != null && yEnumerable != null)
{
foreach (var yItem in yEnumerable)
{
bool match = false;
foreach (var xItem in xEnumerable)
{
if(SafeEqual(xItem, yItem))
{
match = true;
break;
}
}
if (!match)
return false;
}
return true;
}
return x.Equals(y);
}
public int SafeHashCode<T>(T x)
{
if (EqualityComparer<T>.Default.Equals(x, default(T)))
return 0;
return x.GetHashCode();
}
public object SafeClone<T>(T x)
{
//if x is null or default value
if (EqualityComparer<T>.Default.Equals(x, default(T)))
return default(T);
//if x is of type EntityBase call clone()
if (x is EntityBase)
return (x as EntityBase).Clone();
//else the type is a default type return the value
return x;
}
public abstract bool Equals(EntityBase other);
public override abstract int GetHashCode();
public abstract override bool Equals(object obj);
public abstract object Clone();
}
Update I was able to fix it by adding the following code inside SafeHashCode(T x)
IEnumerable<object> xEnumerable = x as IEnumerable<object>;
if (xEnumerable != null)
return xEnumerable.Aggregate(17, (acc, item) => acc * 19 + SafeHashCode(item));
For SaveEqual
you have custom check for IEnumerable
types comparison - you go and check whether each item from xEnumerable
contains in yEnumerable
. Note 1: you also have a bug here - yEnumerable
can contain other items or it can have duplicated items.
But for SaveHashCode
you don't have custom processing for IEnumerable
types. You just return hash code of argument. That will give you different results for different array instances, even if arrays contain same values. To fix that you should calculate hash code based on collection items:
public int SaveHashCode<T>(T x)
{
if (EqualityComparer<T>.Default.Equals(x, default(T)))
return 0;
IEnumerable<object> xEnumerable = x as IEnumerable<object>;
if (xEnumerable != null)
return xEnumerable.Aggregate(17, (acc, item) => acc * 19 + SaveHashCode(item));
return x.GetHashCode();
}
Note 2 using XOR for hashCode calculation gives you another issue - XORing with zero (which you are using as default value, or it can be hash code of integer 0
, or boolean false
) does not modify result. Also result will not depend on order. I.e. if you have two arrays: [0, 1, 2]
and [2,0,1,0]
. XORing as @christophano suggested will give you... 3
and 3
. But those arrays are totally different. So I suggest you to use hashCode calcualtion based on prime numbers.