比较两个对象的相等性,而不必重写Equals或实现IEquatable<>。
你为什么要这么做?当你真的想知道两个对象是否相等,但你懒得重写Equals(object)或实现IEquatable<T>。或者,更现实地说,如果您有一个非常复杂的类,手工实现Equals将非常乏味、容易出错,维护起来也不有趣。如果您不太关心性能,它也会有所帮助。
我目前使用IsEqualTo是因为第二个原因——我有一个具有许多属性的类,这些属性的类型是其他用户定义的类,每个类都有许多其他属性,这些属性的类型是其他用户定义的类,一直到无限。在许多这样的类中加入一堆集合,实现Equals(object)真的变成了一场噩梦。
用法:
if (myTerriblyComplexObject.IsEqualTo(myOtherTerriblyComplexObject))
{
// Do something terribly interesting.
}
为了确定相等,我做了很多比较。我尽一切努力按照“正确”的顺序做“正确”的事。比较顺序如下:
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.
字符串比较可能更好,但很容易实现。此外,我不能100%确定我处理结构正确。
话不多说,下面是扩展方法:
/// <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();
}
}
}