Глубокая структура Equals() для классов .NET?
-
22-08-2019 - |
Вопрос
Учитывая два объекта, которые не содержат внутри себя циклы ссылок, знаете ли вы метод, который проверяет их равенство «общим» способом (посредством отражения)?
По сути, мне нужна та же семантика, что и эквивалентность структур, только для классов.
Решение
Я думаю, что в рамках фреймворка такого метода нет, но его довольно легко написать.Возможно, это не самая короткая реализация, но, похоже, она справляется со своей задачей:
private bool AreEqual(object x, object y)
{
// if both are null, they are equal
if (x == null && y == null)
{
return true;
}
// if one of them are null, they are not equal
else if (x == null || y == null)
{
return false;
}
// if they are of different types, they can't be compared
if (x.GetType() != y.GetType())
{
throw new InvalidOperationException("x and y must be of the same type");
}
Type type = x.GetType();
PropertyInfo[] properties = type.GetProperties();
for (int i = 0; i < properties.Length; i++)
{
// compare only properties that requires no parameters
if (properties[i].GetGetMethod().GetParameters().Length == 0)
{
object xValue = properties[i].GetValue(x, null);
object yValue = properties[i].GetValue(y, null);
if (properties[i].PropertyType.IsValueType && !xValue.Equals(yValue))
{
return false;
}
else if (!properties[i].PropertyType.IsValueType)
{
if (!AreEqual(xValue, yValue))
{
return false;
}
} // if
} // if
} // for
return true;
}
Другие советы
Если ты хочешь это сделать без размышляя над каждым вызовом, вы можете рассмотреть возможность создания DynamicMethod
при первом вызове и вместо этого использовать его.(У меня была ссылка на статью, в которой это делается, но я ее потерял — извините — попробуйте погуглить, если интересно.)
КСТАТИ
Expression.Lambda<Func<T,T,bool>> Compile()
может использоваться как построитель динамических методов.
все равно придется использовать отражение при построении выражения
Вот обновленная версия ответа Фредрика Мёрка, которая учитывает Nullable
и рекурсивные ссылки:
public static bool AreEqual<T>(T x, T y) =>
AreEqual(x, y, new HashSet<object>(new IdentityEqualityComparer<object>()));
private static bool AreEqual(object x, object y, ISet<object> visited)
{
// if both are null, they are equal
if (x == null && y == null) return true;
// if one of them are null, they are not equal
if (x == null || y == null) return false;
// if they are of different types, they can't be compared
if (x.GetType() != y.GetType())
{
throw new InvalidOperationException("x and y must be of the same type");
}
// check for recursive references
if (visited.Contains(x)) return true;
if (visited.Contains(y)) return true;
visited.Add(x);
visited.Add(y);
var type = x.GetType();
var properties = type.GetProperties();
foreach (var property in properties)
{
// compare only properties that requires no parameters
if (property.GetGetMethod().GetParameters().Length == 0)
{
object xValue = property.GetValue(x, null);
object yValue = property.GetValue(y, null);
if (property.PropertyType.IsValueType)
{
// check for Nullable
if (xValue == null && yValue == null) continue;
if (xValue == null || yValue == null) return false;
if (!xValue.Equals(yValue)) return false;
}
if (!property.PropertyType.IsValueType)
{
if (!AreEqual(xValue, yValue, visited)) return false;
}
}
}
return true;
}
private class IdentityEqualityComparer<T> : IEqualityComparer<T> where T : class
{
public int GetHashCode(T value) => RuntimeHelpers.GetHashCode(value);
public bool Equals(T left, T right) => left == right;
}