编写一个(非常)通用的相等比较器

writing a (very) generic equality comparer

我有一个可以直接序列化的数据结构(例如 XML、JSON):有一个主要的 class C1 和其他几个 classes C2, C3, ..., Cn。所有 classes Ci 都有 public 属性,它们是

没有循环引用。

我想定义一个通用的相等比较器 ValueEqualityComparer<T>,它比较任何 classes Ci value-wise,即按照以下规范方式:

我已经了解到像上面这样的模式匹配是不能直接进行的,所以我需要反思。但我正在为如何做到这一点而苦苦挣扎。

这是我到目前为止所写的:

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: 行的问题:

请不要填写 // TODO 部分;我想尽可能多地了解自己。

我不想比较序列化对象,因为它对调试帮助不大。毕竟需要比较器进行测试。

好的,所以花了一点时间,但我想我明白了,这里没什么。


对于基元和字符串

首先,如果它是原始类型或字符串,您可以简单地调用 left.Equals(right)(我决定调用参数 leftright 而不是 xy, 只是因为)


对于 IEnumerables

然后,如果它是可枚举的,事情就会变得更加复杂。首先我们需要为我们的新 ValueEqualityComparer 创建一个泛型类型,然后我们得到它的构造函数并构造一个新的(这一步可以通过缓存(在 Dictionary 中)以前的相等比较器来增强类型,所以如果很多一种类型在另一种类型之间进行比较,我们就不需要每次都创建一个新的比较器)。然后我们需要获取 SequenceEquals 方法,使其成为通用的并使用我们的 leftrightelementComparer.

调用它

对于所有其他类型

对于我们想要将 属性 与 属性 进行比较的每个其他类型,我们首先需要获取给定类型的所有属性。然后我们需要遍历每个 属性,从 leftright 获取 属性 的值,创建我们的泛型 ValueEqualityComparer,获取它的 Equal 方法,最后用我们的 leftProprightProp

调用所述 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 });