编写一个(非常)通用的相等比较器
writing a (very) generic equality comparer
我有一个可以直接序列化的数据结构(例如 XML、JSON):有一个主要的 class C1
和其他几个 classes C2
, C3
, ..., Cn
。所有 classes Ci
都有 public
属性,它们是
- 原始类型或
string
IEnumerable<T>
其中 T
是基本类型或 string
或任何 classes Cj
(其中 j != i
)
Cj
(其中 j != i
)
没有循环引用。
我想定义一个通用的相等比较器 ValueEqualityComparer<T>
,它比较任何 classes Ci
value-wise,即按照以下规范方式:
- 如果类型
T
是原始类型,则使用 Equals
(或 ==
)
- 如果类型
T
是 IEnumerable<S>
,则在 S
对象上使用 Enumerable.SequenceEqual
,并使用 ValueEqualityComparer<S>
作为第三个参数
- 否则,对于每个 public 属性
P
使用 ValueEqualityComparer<P>.Equals
并通过 &&
. 连接这些结果
我已经了解到像上面这样的模式匹配是不能直接进行的,所以我需要反思。但我正在为如何做到这一点而苦苦挣扎。
这是我到目前为止所写的:
public class ValueEqualityComparer<T> : IEqualityComparer<T>
{
public static readonly ValueEqualityComparer<T> Get = new ValueEqualityComparer<T>();
private static readonly Type _type = typeof(T);
private static readonly bool _isPrimitiveOrString = IsPrimitiveOrString(_type);
private static readonly Type _enumerableElementType = GetEnumerableElementTypeOrNull(_type);
private static bool _isEnumerable => _enumerableElementType != null;
private ValueEqualityComparer() {}
public bool Equals(T x, T y)
{
if (x == null || y == null)
{
if (x == null && y == null)
return true;
return false;
}
if (_isPrimitive)
return x.Equals(y);
if (_isEnumerable)
{
var comparerType = typeof(ValueEqualityComparer<>).MakeGenericType(new Type[] { _enumerableElementType });
var elementComparer = comparerType.GetField("Get").GetValue(null);
// not sure about this line:
var result = Expression.Call(typeof(Enumerable), "SequenceEqual", new Type[] { _enumerableElementType },
new Expression[] { Expression.Constant(x), Expression.Constant(y), Expression.Constant(elementComparer) });
}
// TODO: iterate public properties, use corresponding ValueEqualityComparers
}
public int GetHashCode(T obj)
{
// TODO
}
private static bool IsPrimitiveOrString(Type t) => t.IsPrimitive || t == typeof(string);
// if we have e.g. IEnumerable<string>, it will return string
private static Type GetEnumerableElementTypeOrNull(Type t)
{
Type enumerableType = t.GetInterfaces().Where(i => i.IsGenericType
&& i.GetGenericTypeDefinition() == typeof(IEnumerable<>)).FirstOrDefault();
return enumerableType?.GetGenericArguments().Single();
}
}
关于 // not sure about this line:
行的问题:
- 目标是调用
Enumerable.SequenceEqual<S>(x, y, ValueEqualityComparer<S>.Get)
,其中 S
是 x
和 y
的元素类型(即 T
是 IEnumerable<S>
).我为此目的写的行是否正确?
- 如何获得该调用的结果(即
true
或 false
)?
请不要填写 // TODO
部分;我想尽可能多地了解自己。
我不想比较序列化对象,因为它对调试帮助不大。毕竟需要比较器进行测试。
好的,所以花了一点时间,但我想我明白了,这里没什么。
对于基元和字符串
首先,如果它是原始类型或字符串,您可以简单地调用 left.Equals(right)
(我决定调用参数 left
和 right
而不是 x
和 y
, 只是因为)
对于 IEnumerables
然后,如果它是可枚举的,事情就会变得更加复杂。首先我们需要为我们的新 ValueEqualityComparer
创建一个泛型类型,然后我们得到它的构造函数并构造一个新的(这一步可以通过缓存(在 Dictionary
中)以前的相等比较器来增强类型,所以如果很多一种类型在另一种类型之间进行比较,我们就不需要每次都创建一个新的比较器)。然后我们需要获取 SequenceEquals
方法,使其成为通用的并使用我们的 left
、right
和 elementComparer
.
调用它
对于所有其他类型
对于我们想要将 属性 与 属性 进行比较的每个其他类型,我们首先需要获取给定类型的所有属性。然后我们需要遍历每个 属性,从 left
和 right
获取 属性 的值,创建我们的泛型 ValueEqualityComparer
,获取它的 Equal
方法,最后用我们的 leftProp
和 rightProp
调用所述 Equal
方法
现在一起:
这导致 class:
public class ValueEqualityComparer<T> : IEqualityComparer<T>
{
private readonly Type _enumerableElementType;
private readonly bool _isEnumerable;
private readonly bool _isPrimitiveOrString;
public ValueEqualityComparer()
{
var type = typeof(T);
_isPrimitiveOrString = IsPrimitiveOrString(type);
// Only check if it's an enumerable,
// if the current type to compare is not a primitive or string
if (!_isPrimitiveOrString)
(_isEnumerable, _enumerableElementType) = IsEnumerableAndElementType(type);
}
public bool Equals(T left, T right)
{
if (_isPrimitiveOrString)
return left?.Equals(right) ?? false;
if (_isEnumerable)
{
// Make generic ValueEqualityComparer type for type of element of enumerable and construct it
// Possibly cache this
var elementComparer = typeof(ValueEqualityComparer<>).MakeGenericType(_enumerableElementType)
.GetConstructor(new Type[] { })?.Invoke(null);
// Get the SequenceEqual method and make it generic for our element type
// SequenceEqual methods may also be cached for better performance
var sequenceEquals = typeof(Enumerable).Assembly.GetTypes()
.Where(assemblyType =>
assemblyType.IsSealed && !assemblyType.IsGenericType && !assemblyType.IsNested)
.SelectMany(assemblyType => assemblyType.GetMethods(BindingFlags.Static | BindingFlags.Public),
(assemblyType, method) => new {assemblyType, method})
.Where(x => x.method.IsDefined(typeof(ExtensionAttribute), false))
.First(x => x.method.Name == nameof(Enumerable.SequenceEqual) &&
x.method.GetParameters().Length == 3)
.method
.MakeGenericMethod(_enumerableElementType);
// This is basically like calling left.SequenceEqual(right, elementComparer);
// Cast to bool, as SequenceEqual returns a bool
return (bool) sequenceEquals.Invoke(null, new[] {left, right, elementComparer});
}
// T is not an Enumerable, and not a primitive or a string, so get all public instance properties and compare them
// We ignore private and static properties here
var properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
foreach (var property in properties)
{
// Get the values of left and right property
var leftProp = property.GetValue(left);
var rightProp = property.GetValue(right);
var propComparerType = typeof(ValueEqualityComparer<>).MakeGenericType(property.PropertyType);
var propComparer = propComparerType.GetConstructor(new Type[] { })?.Invoke(null);
var equalsMethod = propComparerType.GetMethod(nameof(Equals),
new[] {property.PropertyType, property.PropertyType});
if (equalsMethod == null)
continue;
// If any of the properties don't equal one another, return false early
if (!(bool) equalsMethod.Invoke(propComparer, new[] {leftProp, rightProp}))
return false;
}
return true;
}
public int GetHashCode(T x)
{
return Tuple.Create(_isEnumerable, _enumerableElementType, _isPrimitiveOrString)
.GetHashCode();
}
private static bool IsPrimitiveOrString(Type t)
{
return t.IsPrimitive || t == typeof(string);
}
private static (bool, Type) IsEnumerableAndElementType(Type t)
{
var enumerableType = t.GetInterfaces()
.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>));
return (enumerableType != null, enumerableType?.GetGenericArguments().Single());
}
}
Here's 一个 link 到一个包含这个的 dotnetfiddle,以及一些测试
感谢 MindSwipe 的评论,我明白了。只需用以下代码片段替换 // not sure about this line:
行:
// generic methods cannot be retrieved by the current GetMethod() api, this is a common workaround:
var sequenceEqualNongeneric = typeof(Enumerable).GetMethods().Where(
m => m.Name == "SequenceEqual" && m.GetParameters().Length == 3).Single();
var sequenceEqualGeneric = sequenceEqualNongeneric.MakeGenericMethod(new Type[] { _enumerableElementType });
return (bool)sequenceEqualGeneric.Invoke(null, new object[] { x, y, elementComparer });
我有一个可以直接序列化的数据结构(例如 XML、JSON):有一个主要的 class C1
和其他几个 classes C2
, C3
, ..., Cn
。所有 classes Ci
都有 public
属性,它们是
- 原始类型或
string
IEnumerable<T>
其中T
是基本类型或string
或任何 classesCj
(其中j != i
)Cj
(其中j != i
)
没有循环引用。
我想定义一个通用的相等比较器 ValueEqualityComparer<T>
,它比较任何 classes Ci
value-wise,即按照以下规范方式:
- 如果类型
T
是原始类型,则使用Equals
(或==
) - 如果类型
T
是IEnumerable<S>
,则在S
对象上使用Enumerable.SequenceEqual
,并使用ValueEqualityComparer<S>
作为第三个参数 - 否则,对于每个 public 属性
P
使用ValueEqualityComparer<P>.Equals
并通过&&
. 连接这些结果
我已经了解到像上面这样的模式匹配是不能直接进行的,所以我需要反思。但我正在为如何做到这一点而苦苦挣扎。
这是我到目前为止所写的:
public class ValueEqualityComparer<T> : IEqualityComparer<T>
{
public static readonly ValueEqualityComparer<T> Get = new ValueEqualityComparer<T>();
private static readonly Type _type = typeof(T);
private static readonly bool _isPrimitiveOrString = IsPrimitiveOrString(_type);
private static readonly Type _enumerableElementType = GetEnumerableElementTypeOrNull(_type);
private static bool _isEnumerable => _enumerableElementType != null;
private ValueEqualityComparer() {}
public bool Equals(T x, T y)
{
if (x == null || y == null)
{
if (x == null && y == null)
return true;
return false;
}
if (_isPrimitive)
return x.Equals(y);
if (_isEnumerable)
{
var comparerType = typeof(ValueEqualityComparer<>).MakeGenericType(new Type[] { _enumerableElementType });
var elementComparer = comparerType.GetField("Get").GetValue(null);
// not sure about this line:
var result = Expression.Call(typeof(Enumerable), "SequenceEqual", new Type[] { _enumerableElementType },
new Expression[] { Expression.Constant(x), Expression.Constant(y), Expression.Constant(elementComparer) });
}
// TODO: iterate public properties, use corresponding ValueEqualityComparers
}
public int GetHashCode(T obj)
{
// TODO
}
private static bool IsPrimitiveOrString(Type t) => t.IsPrimitive || t == typeof(string);
// if we have e.g. IEnumerable<string>, it will return string
private static Type GetEnumerableElementTypeOrNull(Type t)
{
Type enumerableType = t.GetInterfaces().Where(i => i.IsGenericType
&& i.GetGenericTypeDefinition() == typeof(IEnumerable<>)).FirstOrDefault();
return enumerableType?.GetGenericArguments().Single();
}
}
关于 // not sure about this line:
行的问题:
- 目标是调用
Enumerable.SequenceEqual<S>(x, y, ValueEqualityComparer<S>.Get)
,其中S
是x
和y
的元素类型(即T
是IEnumerable<S>
).我为此目的写的行是否正确? - 如何获得该调用的结果(即
true
或false
)?
请不要填写 // TODO
部分;我想尽可能多地了解自己。
我不想比较序列化对象,因为它对调试帮助不大。毕竟需要比较器进行测试。
好的,所以花了一点时间,但我想我明白了,这里没什么。
对于基元和字符串
首先,如果它是原始类型或字符串,您可以简单地调用 left.Equals(right)
(我决定调用参数 left
和 right
而不是 x
和 y
, 只是因为)
对于 IEnumerables
然后,如果它是可枚举的,事情就会变得更加复杂。首先我们需要为我们的新 ValueEqualityComparer
创建一个泛型类型,然后我们得到它的构造函数并构造一个新的(这一步可以通过缓存(在 Dictionary
中)以前的相等比较器来增强类型,所以如果很多一种类型在另一种类型之间进行比较,我们就不需要每次都创建一个新的比较器)。然后我们需要获取 SequenceEquals
方法,使其成为通用的并使用我们的 left
、right
和 elementComparer
.
对于所有其他类型
对于我们想要将 属性 与 属性 进行比较的每个其他类型,我们首先需要获取给定类型的所有属性。然后我们需要遍历每个 属性,从 left
和 right
获取 属性 的值,创建我们的泛型 ValueEqualityComparer
,获取它的 Equal
方法,最后用我们的 leftProp
和 rightProp
Equal
方法
现在一起:
这导致 class:
public class ValueEqualityComparer<T> : IEqualityComparer<T>
{
private readonly Type _enumerableElementType;
private readonly bool _isEnumerable;
private readonly bool _isPrimitiveOrString;
public ValueEqualityComparer()
{
var type = typeof(T);
_isPrimitiveOrString = IsPrimitiveOrString(type);
// Only check if it's an enumerable,
// if the current type to compare is not a primitive or string
if (!_isPrimitiveOrString)
(_isEnumerable, _enumerableElementType) = IsEnumerableAndElementType(type);
}
public bool Equals(T left, T right)
{
if (_isPrimitiveOrString)
return left?.Equals(right) ?? false;
if (_isEnumerable)
{
// Make generic ValueEqualityComparer type for type of element of enumerable and construct it
// Possibly cache this
var elementComparer = typeof(ValueEqualityComparer<>).MakeGenericType(_enumerableElementType)
.GetConstructor(new Type[] { })?.Invoke(null);
// Get the SequenceEqual method and make it generic for our element type
// SequenceEqual methods may also be cached for better performance
var sequenceEquals = typeof(Enumerable).Assembly.GetTypes()
.Where(assemblyType =>
assemblyType.IsSealed && !assemblyType.IsGenericType && !assemblyType.IsNested)
.SelectMany(assemblyType => assemblyType.GetMethods(BindingFlags.Static | BindingFlags.Public),
(assemblyType, method) => new {assemblyType, method})
.Where(x => x.method.IsDefined(typeof(ExtensionAttribute), false))
.First(x => x.method.Name == nameof(Enumerable.SequenceEqual) &&
x.method.GetParameters().Length == 3)
.method
.MakeGenericMethod(_enumerableElementType);
// This is basically like calling left.SequenceEqual(right, elementComparer);
// Cast to bool, as SequenceEqual returns a bool
return (bool) sequenceEquals.Invoke(null, new[] {left, right, elementComparer});
}
// T is not an Enumerable, and not a primitive or a string, so get all public instance properties and compare them
// We ignore private and static properties here
var properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
foreach (var property in properties)
{
// Get the values of left and right property
var leftProp = property.GetValue(left);
var rightProp = property.GetValue(right);
var propComparerType = typeof(ValueEqualityComparer<>).MakeGenericType(property.PropertyType);
var propComparer = propComparerType.GetConstructor(new Type[] { })?.Invoke(null);
var equalsMethod = propComparerType.GetMethod(nameof(Equals),
new[] {property.PropertyType, property.PropertyType});
if (equalsMethod == null)
continue;
// If any of the properties don't equal one another, return false early
if (!(bool) equalsMethod.Invoke(propComparer, new[] {leftProp, rightProp}))
return false;
}
return true;
}
public int GetHashCode(T x)
{
return Tuple.Create(_isEnumerable, _enumerableElementType, _isPrimitiveOrString)
.GetHashCode();
}
private static bool IsPrimitiveOrString(Type t)
{
return t.IsPrimitive || t == typeof(string);
}
private static (bool, Type) IsEnumerableAndElementType(Type t)
{
var enumerableType = t.GetInterfaces()
.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == typeof(IEnumerable<>));
return (enumerableType != null, enumerableType?.GetGenericArguments().Single());
}
}
Here's 一个 link 到一个包含这个的 dotnetfiddle,以及一些测试
感谢 MindSwipe 的评论,我明白了。只需用以下代码片段替换 // not sure about this line:
行:
// generic methods cannot be retrieved by the current GetMethod() api, this is a common workaround:
var sequenceEqualNongeneric = typeof(Enumerable).GetMethods().Where(
m => m.Name == "SequenceEqual" && m.GetParameters().Length == 3).Single();
var sequenceEqualGeneric = sequenceEqualNongeneric.MakeGenericMethod(new Type[] { _enumerableElementType });
return (bool)sequenceEqualGeneric.Invoke(null, new object[] { x, y, elementComparer });