【问题标题】:writing a (very) generic equality comparer编写一个(非常)通用的相等比较器
【发布时间】:2020-09-22 07:48:05
【问题描述】:

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

  • 原始类型或string
  • IEnumerable<T> 其中T 是原始类型或string 或任何类Cj(其中j != i
  • Cj(其中j != i

没有循环引用。

我想定义一个通用相等比较器 ValueEqualityComparer<T>,它比较任何类 Ci 值,即以以下规范方式:

  • 如果T 是原始类型,则使用Equals(或==
  • 如果T 类型为IEnumerable<S>,则在S 对象上使用Enumerable.SequenceEqual,并将ValueEqualityComparer<S> 作为第三个参数
  • 否则,对于每个公共属性 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&lt;S&gt;(x, y, ValueEqualityComparer&lt;S&gt;.Get),其中Sxy 的元素类型(即TIEnumerable&lt;S&gt;)。我为此写的那一行是否正确?
  • 如何获得该调用的结果(即truefalse)?

请不要填写// TODO部分;我想尽我所能为自己弄清楚。

我不想比较序列化的对象,因为它对调试没有多大帮助。毕竟,比较器是测试所必需的。

【问题讨论】:

  • 您所说的规则中没有任何内容阻止Cx 拥有CyCy 拥有Cx。这种情况(或更大的周期)实际上是否允许?如果是这样,您将如何防止无限回归?
  • 关于你所说的“所有类Ci 都有公共属性,它们是......”你的意思是不会有循环引用吗?从您当前编写的方式来看,循环引用仍然是可能的。 C1 可以有一个C2 属性,C2 可以有一个C1 属性,两个对象可以互相引用,这可能会导致无限递归。
  • 为什么不简单地同时序列化xy并比较序列化结果呢?
  • 我不认为Expression.Call 是正确的,IMO 你应该得到SequenceEqualMethodInfo 然后使用MethodInfo.Invoke
  • return Equals(x, y) 看起来有问题.. 可能是无限循环。如果我尝试这样做,我想我会使用重载方法、强制转换和审查多态行为.. 可能还有 Tuple.Create for GetHashCode 重载方法..

标签: c# generics reflection expression


【解决方案1】:

好的,所以玩了一会儿,但我想我明白了,这里什么都没有。


对于原语和字符串

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


对于 IEnumerables

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


对于所有其他类型

对于我们想要逐个比较属性的所有其他类型,我们首先需要获取给定类型的所有属性。然后我们需要遍历每个属性,从leftright获取属性的值,创建我们的通用ValueEqualityComparer,获取它的Equal方法,最后用我们的leftProp调用Equal方法和rightProp


现在大家一起:

这导致了这个类:

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 指向包含此内容的 dotnetfiddle 的链接,以及一些测试

【讨论】:

  • 你说“之后我们得到它的构造函数并构造一个新的(这一步可以通过缓存(在字典中)以前的相等比较器按类型来增强,所以如果很多一个类型得到一个接一个地进行比较,我们不需要每次都创建一个新的比较器)”——因此,我将大多数东西设为静态并使用静态 Get() 方法而不是构造函数。
  • 好吧,在你每次调用Get() 时都会调用的构造函数中,无论如何你都要重新创建它。因此,不使用Get() 确实没有任何区别。我的意思是一个私有静态字典,它缓存ValueEqualityComparer&lt;T&gt;,其中密钥为T,如果实现,Get() 将有理由存在,即首先检查缓存,然后只创建一个新的如果它还没有被缓存。但正如我现在的回答,没有理由使用Get() 方法
  • 感谢您的提示,我的意思是为Get 设置一个静态字段,而不是方法。更正了它。这样,构造函数会在每个实际使用的T 中调用一次,而后续调用只是重用它。现在不需要字典了。
  • @Kjara 我运行了一个快速基准测试,其中一个版本实现了 Get 和一个基本的 LRU 缓存,一个版本实现了具有静态属性的 Get,一个没有任何花哨的东西。结果是:Cached 的平均值为 234.3µs,误差为 4.32µs,Static Get 的平均值为 227.8µs,误差为 4.26µs,而良好的常规构造每次平均为 227.2µs,误差为 1.93 µs(当然越低越好)
  • 还应该包括IEnumerable&lt;T&gt;.IntersectIEnumerable&lt;T&gt;.Except 你的测试,通过IEqualityComparer&lt;T&gt; 实例(我相信这将测试GetHashCode 实现)
【解决方案2】:

感谢 MindSwipe 的评论,我明白了。只需将// not sure about this line: 行替换为以下代码 sn-p:

// 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 });

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-03-30
    • 2020-05-13
    • 2019-03-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-22
    相关资源
    最近更新 更多