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

2

5

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

Nursling answered 27/1, 2020 at 13:57 Comment(7)
Obviously, you'd want to use SequenceEqual() to compare the IEnumerable<T> - which, in turn, will use the T's default equality comparer for comparing the items, which again will be ReferenceEquals if T is a reference type...Counterirritant
@GSerg: Well, that is true. But when I put an object under my control into a list / an enumerable then I can make sure to implement IEquatable for this object. But I cannot change the fact that Microsoft did not bother doing this for IEnumerable<T>.Nursling
Effectively you want a way to write, var castedThis = (IEnumerable<U>)Convert.ChangeType(Parameter, enumerableType);, where U is dynamic (and T is IEnumerable<U>). I believe the only way to do that would be to declare both T and U as generic arguments for your class.Counterirritant
Side note: as T itself is IEnumerable<>, .GetInterfaces() on it will only return the non-generic IEnumerable (and because you have type => type.IsGenericType in the filter, the result will always be empty). To get enumerableType, just use parameterType.GetGenericArguments().First(). Not that it would help with the above.Counterirritant
@GSerg: Well, actually so far it works. When Parameter is an IList<int>, for example, enumerableType is identified correctly as an int: {Name = "Int32" FullName = "System.Int32"} But as you said: not that it would help ;-)Nursling
Ah, when the parameter is an IList<int>, yes. But not when it's an IEnumerable<int> directly.Counterirritant
I think you should be very careful on this, lots of things implement IEnumerable<T> for some T, but are not solely containers. If T is string then your doing ordinal comparison now. Dictionary<T, K> is IEnumerable but 2 SequenceEqual dictionaries can have different compares so are not fungible. I have worked on many tree classes that are IEnumerable of their children, but have tag info your idea would ignore. Tldr: not safe to assume declaring IEnumerable<X> implies 'I am ONLY an IEnumerable<X>' .Michealmicheil
P
4

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.

Parting answered 27/1, 2020 at 19:50 Comment(1)
Thanks! I like this solution. Especially as all other possible solutions (i.e. figuring out the "right" comparer within the class itself) are kind of ugly/smelly ;-)Nursling
C
5

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.

Counterirritant answered 27/1, 2020 at 15:40 Comment(0)
P
4

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.

Parting answered 27/1, 2020 at 19:50 Comment(1)
Thanks! I like this solution. Especially as all other possible solutions (i.e. figuring out the "right" comparer within the class itself) are kind of ugly/smelly ;-)Nursling

© 2022 - 2024 — McMap. All rights reserved.