mardi 22 septembre 2020

writing an (very) generic equality comparer

I have a data stucture that is straightforward serializable (e.g. to XML, to JSON): There is a main class C1 and several other classes C2, C3, ..., Cn. All classes Ci have public properties which are either

  • primitive types or string
  • IEnumerable<T> where T is either primitive type or string or any of the classes Cj (where j != i)
  • Cj (where j != i)

I want to define a generic equality comparer ValueEqualityComparer<T> which compares any of the classes Ci value-wise, i.e. in the following canonical way:

  • if type T is primitive, use Equals (or ==)
  • if type T is IEnumerable<S>, use Enumerable.SequenceEqual on the S objects with a ValueEqualityComparer<S> as third argument
  • else, for each public property P use ValueEqualityComparer<P>.Equals and connect these results via &&.

I already learned that pattern matching like above is not possible directly, so I need reflection. But I am struggling with how to do that.

Here is what I have written so far:

public class ValueEqualityComparer<T> : IEqualityComparer<T>
{
    public static readonly ValueEqualityComparer<T> Get = new ValueEqualityComparer<T>();
    private static readonly Type _type;
    private static readonly bool _isPrimitiveOrString;
    private static readonly Type _enumerableElementType;
    private static bool _isEnumerable => _enumerableElementType != null;

    static ValueEqualityComparer()
    {
        _type = typeof(T);
        _isPrimitiveOrString = IsPrimitiveOrString(_type);
        _enumerableElementType = GetEnumerableElementTypeOrNull(_type);
    }

    private ValueEqualityComparer() {}

    public bool Equals(T x, T y)
    {
        if (_isPrimitive)
            return Equals(x, y);

        if (_isEnumerable)
        {
            var comparerType = typeof(ValueEqualityComparer<>).MakeGenericType(new Type[] { _enumerableElementType });
            var elementComparer = comparerType.GetMethod("Get").Invoke(null, 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
    }


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

Questions regarding the // not sure about this line: line:

  • The goal is to call Enumerable.SequenceEqual<S>(x, y, ValueEqualityComparer<S>.Get()) where S is the element type of x and y (i.e. T is IEnumerable<S>). Is the line I wrote correct for that purpose?
  • How do I get the result (i.e. true or false) of that call?

Please don't fill in the // TODO section; I want to figure out as much as I can for myself.





Aucun commentaire:

Enregistrer un commentaire