Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to compare two IEnumerable<T> in C# if I don't know the actual object type?

Tags:

c#

iequatable

I'm struggling with implementing the IEquatable<> interface for a class. The class has a Parameter property that uses a generic type. Basically the class definition is like this:

public class MyClass<T> : IEquatable<MyClass<T>>
{
    public T Parameter { get; }

    ...
}

In the Equals() method I'm using EqualityComparer<T>.Default.Equals(Parameter, other.Parameter) to compare the property. Generally, this works fine – as long as the property is not a collection, for example an IEnumerable<T>. The problem is that the default equality comparer for IEnumerable<T> is checking reference equality.

Obviously, you'd want to use SequenceEqual() to compare the IEnumerable<T>. But to get this running, you need to specify the generic type of the SequenceEqual() method. This is the closest I could get:

var parameterType = typeof(T);
var enumerableType = parameterType.GetInterfaces()
    .Where(type => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
    .Select(type => type.GetGenericArguments().First()).FirstOrDefault();

if (enumerableType != null)
{
    var castedThis = Convert.ChangeType(Parameter, enumerableType);
    var castedOther = Convert.ChangeType(other.Parameter, enumerableType);

    var isEqual = castedThis.SequenceEqual(castedOther);
}

But this does not work because Convert.ChangeType() returns an object. And of course object does not implement SequenceEqual().

How do I get this working? Thanks for any tipps!

Best regards, Oliver

like image 445
Baldewin Avatar asked Jan 27 '20 13:01

Baldewin


2 Answers

Given that you have a generic container that you want to compare various generic items, you don't want to be hard coding in various specific equality checks for certain types. There are going to be lots of situations where the default equality comparison won't work for what some particular caller is trying to do. The comments have numerous different examples of problems that can come up, but also just consider the many many classes out there who's default equality is a reference comparison by for which someone might want a value comparison. You can't have this equality comparer just hard code in a solution for all of those types.

The solution of course is easy. Let the caller provide their own equality implementation, which in C#, means an IEqualityComparer<T>. Your class can become:

public class MyClass<T> : IEquatable<MyClass<T>>
{
    private IEqualityComparer<T> comparer;

    public MyClass(IEqualityComparer<T> innerComparer = null)
    {
        comparer = innerComparer ?? EqualityComparer<T>.Default;
    }

    public T Parameter { get; }

    ...
}

And now by default the default comparer will be used for any given type, but the caller can always specify a non-default comparer for any type that needs different equality semantics.

like image 172
Servy Avatar answered Sep 21 '22 17:09

Servy


Effectively you want a way to say

var castedThis = (IEnumerable<U>)Convert.ChangeType(Parameter, enumerableType);

where T is IEnumerable<U> and U is dynamic.

I don't think you can do that.

If you are happy with some boxing though, you can use the non-generic IEnumerable interface:

public bool Equals(MyClass<T> other)
{
    var parameterType = typeof(T);

    if (typeof(IEnumerable).IsAssignableFrom(parameterType))
    {
        var castedThis = ((IEnumerable)this.Parameter).GetEnumerator();
        var castedOther = ((IEnumerable)other.Parameter).GetEnumerator();

        try
        {
            while (castedThis.MoveNext())
            {
                if (!castedOther.MoveNext())
                    return false;

                if (!Convert.Equals(castedThis.Current, castedOther.Current))
                    return false;
            }

            return !castedOther.MoveNext();
        }
        finally
        {
            (castedThis as IDisposable)?.Dispose();
            (castedOther as IDisposable)?.Dispose();
        }
    }
    else
    {
        return EqualityComparer<T>.Default.Equals(this.Parameter, other.Parameter);
    }
}

If you are not happy with the boxing, then you can use reflection to construct and call SequenceEqual (as inspired by How do I invoke an extension method using reflection?):

public bool Equals(MyClass<T> other)
{
    var parameterType = typeof(T);

    if (typeof(IEnumerable).IsAssignableFrom(parameterType))
    {
        var enumerableType = parameterType.GetGenericArguments().First();

        var sequenceEqualMethod = typeof(Enumerable)
            .GetMethods(BindingFlags.Static | BindingFlags.Public)
            .Where(mi => {
                if (mi.Name != "SequenceEqual")
                    return false;

                if (mi.GetGenericArguments().Length != 1)
                    return false;

                var pars = mi.GetParameters();
                if (pars.Length != 2)
                    return false;

                return pars[0].ParameterType.IsGenericType && pars[0].ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>) && pars[1].ParameterType.IsGenericType && pars[1].ParameterType.GetGenericTypeDefinition() == typeof(IEnumerable<>);
            })
            .First()
            .MakeGenericMethod(enumerableType)
        ;

        return (bool)sequenceEqualMethod.Invoke(this.Parameter, new object[] { this.Parameter, other.Parameter });
    }
    else
    {
        return EqualityComparer<T>.Default.Equals(this.Parameter, other.Parameter);
    }
}

You can cache the sequenceEqualMethod for better performance.

like image 45
GSerg Avatar answered Sep 21 '22 17:09

GSerg