Compare the equality of two objects without (necessarily) overriding Equals or implementing IEquatable<>.
Why would you want to do this? When you really want to know if two objects are equal, but you're too lazy to override Equals(object)
or implement IEquatable<T>
. Or, more realistically, if you have a terribly complex class and implementing Equals by hand would be extremely tedious, error prone, and not fun to maintain. It also helps if you don't care too much about performance.
I am currently using IsEqualTo
because of the second reason - I have a class with many properties whose types are other user-defined classes, each of which has many other properties whose types are other user-defined classes, ad infinitum. Throw in a bunch of collections in many of these classes, and implementing Equals(object)
truly becomes a nightmare.
Usage:
if (myTerriblyComplexObject.IsEqualTo(myOtherTerriblyComplexObject))
{
// Do something terribly interesting.
}
In order to determine equality, I make numerous comparisons. I make every attempt to do the "right" one in the "right" order. The comparisons, in order are:
- Use the static
Equals(object, object)
method. If it returns true, return true. It will return true if the references are the same. It will also return true if thisObject
overrides Equals(object)
.
- If
thisObject
is null, return false. No further comparisons can be made if it is null.
- If
thisObject
has overridden Equals(object)
, return false. Since it overrides Equals, it must mean that Equals was executed at step #1 and returned false. If someone has bothered to override Equals, we should respect that and return what Equals returns.
- If
thisObject
inherits from IEquatable<T>
, where otherObject
can be assigned to T
, get the Equals(T)
method using reflection. Invoke that method and return its return value.
- If both objects are
IEnumerable
, return whether contain the same items, in the same order, using IsEqualTo to compare the items.
- If the objects have different types, return false. Since we know now that
thisObject
does not have an Equals method, there isn't any way to realistically evaluate two object of different types to be true.
- If the objects are a value type (primitive or struct) or a string, return false. We have already failed the
Equals(object)
test - enough said.
- For each property of
thisObject
, test its value with IsEqualTo. If any return false, return false. If all return true, return true.
String comparisons could be better, but easy to implement. Also, I'm not 100% sure I'm handling structs right.
Without further ado, here is the extension method:
/// <summary>
/// Provides extension methods to determine if objects are equal.
/// </summary>
public static class EqualsEx
{
/// <summary>
/// The <see cref="Type"/> of <see cref="string"/>.
/// </summary>
private static readonly Type StringType = typeof(string);
/// <summary>
/// The <see cref="Type"/> of <see cref="object"/>.
/// </summary>
private static readonly Type ObjectType = typeof(object);
/// <summary>
/// The <see cref="Type"/> of <see cref="IEquatable{T}"/>.
/// </summary>
private static readonly Type EquatableType = typeof(IEquatable<>);
/// <summary>
/// Determines whether <paramref name="thisObject"/> is equal to <paramref name="otherObject"/>.
/// </summary>
/// <param name="thisObject">
/// This object.
/// </param>
/// <param name="otherObject">
/// The other object.
/// </param>
/// <returns>
/// True, if they are equal, otherwise false.
/// </returns>
public static bool IsEqualTo(this object thisObject, object otherObject)
{
if (Equals(thisObject, otherObject))
{
// Always check Equals first. If the object has overridden Equals, use it. This will also capture the case where both are the same reference.
return true;
}
if (thisObject == null)
{
// Because Equals(object, object) returns true if both are null, if either is null, return false.
return false;
}
var thisObjectType = thisObject.GetType();
var equalsMethod = thisObjectType.GetMethod("Equals", BindingFlags.Public | BindingFlags.Instance, null, new[] { ObjectType }, null);
if (equalsMethod.DeclaringType == thisObjectType)
{
// thisObject overrides Equals, and we have already failed the Equals test, so return false.
return false;
}
var otherObjectType = otherObject == null ? null : otherObject.GetType();
// If thisObject inherits from IEquatable<>, and otherObject can be passed into its Equals method, use it.
var equatableTypes = thisObjectType.GetInterfaces().Where( // Get interfaces of thisObjectType that...
i => i.IsGenericType // ...are generic...
&& i.GetGenericTypeDefinition() == EquatableType // ...and are IEquatable of some type...
&& (otherObjectType == null || i.GetGenericArguments()[0].IsAssignableFrom(otherObjectType))); // ...and otherObjectType can be assigned to the IEquatable's type.
if (equatableTypes.Any())
{
// If we found any interfaces that meed our criteria, invoke the Equals method for each interface.
// If any return true, return true. If all return false, return false.
return equatableTypes
.Select(equatableType => equatableType.GetMethod("Equals", BindingFlags.Public | BindingFlags.Instance))
.Any(equatableEqualsMethod => (bool)equatableEqualsMethod.Invoke(thisObject, new[] { otherObject }));
}
if (thisObjectType != StringType && thisObject is IEnumerable && otherObject is IEnumerable)
{
// If both are IEnumerable, check their items.
var thisEnumerable = ((IEnumerable)thisObject).Cast<object>();
var otherEnumerable = ((IEnumerable)otherObject).Cast<object>();
return thisEnumerable.SequenceEqual(otherEnumerable, IsEqualToComparer.Instance);
}
if (thisObjectType != otherObjectType)
{
// If they have different types, they cannot be equal.
return false;
}
if (thisObjectType.IsValueType || thisObjectType == StringType)
{
// If it is a value type, we have already determined that they are not equal, so return false.
return false;
}
// Recurse into each public property: if any are not equal, return false. If all are true, return true.
return !(from propertyInfo in thisObjectType.GetProperties()
let thisPropertyValue = propertyInfo.GetValue(thisObject, null)
let otherPropertyValue = propertyInfo.GetValue(otherObject, null)
where !thisPropertyValue.IsEqualTo(otherPropertyValue)
select thisPropertyValue).Any();
}
/// <summary>
/// A <see cref="IEqualityComparer{T}"/> to be used when comparing sequences of collections.
/// </summary>
private class IsEqualToComparer : IEqualityComparer<object>
{
/// <summary>
/// The singleton instance of <see cref="IsEqualToComparer"/>.
/// </summary>
public static readonly IsEqualToComparer Instance;
/// <summary>
/// Initializes static members of the <see cref="EqualsEx.IsEqualToComparer"/> class.
/// </summary>
static IsEqualToComparer()
{
Instance = new IsEqualToComparer();
}
/// <summary>
/// Prevents a default instance of the <see cref="EqualsEx.IsEqualToComparer"/> class from being created.
/// </summary>
private IsEqualToComparer()
{
}
/// <summary>
/// Determines whether the specified objects are equal.
/// </summary>
/// <param name="x">
/// The first object to compare.
/// </param>
/// <param name="y">
/// The second object to compare.
/// </param>
/// <returns>
/// true if the specified objects are equal; otherwise, false.
/// </returns>
bool IEqualityComparer<object>.Equals(object x, object y)
{
return x.IsEqualTo(y);
}
/// <summary>
/// Not implemented - throws an <see cref="NotImplementedException"/>.
/// </summary>
/// <param name="obj">
/// The <see cref="object"/> for which a hash code is to be returned.
/// </param>
/// <returns>
/// A hash code for the specified object.
/// </returns>
int IEqualityComparer<object>.GetHashCode(object obj)
{
throw new NotImplementedException();
}
}
}