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

Baldewin
  • 1,613
  • 2
  • 16
  • 23
  • 2
    `Obviously, you'd want to use SequenceEqual() to compare the IEnumerable` - 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... – GSerg Jan 27 '20 at 14:02
  • @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```. – Baldewin Jan 27 '20 at 14:14
  • Effectively you want a way to write, `var castedThis = (IEnumerable)Convert.ChangeType(Parameter, enumerableType);`, where `U` is dynamic (and `T` is `IEnumerable`). I believe the only way to do that would be to declare both `T` and `U` as generic arguments for your class. – GSerg Jan 27 '20 at 14:25
  • 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. – GSerg Jan 27 '20 at 14:43
  • @GSerg: Well, actually so far it works. When ```Parameter``` is an ```IList```, for example, ```enumerableType``` is identified correctly as an ```int```: ```{Name = "Int32" FullName = "System.Int32"}``` But as you said: not that it would help ;-) – Baldewin Jan 27 '20 at 15:24
  • Ah, when the parameter is an `IList`, yes. But not when it's an `IEnumerable` directly. – GSerg Jan 27 '20 at 15:27
  • 1
    I think you should be very careful on this, lots of things implement IEnumerable for some T, but are not _solely_ containers. If T is string then your doing ordinal comparison now. Dictionary 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 implies 'I am ONLY an IEnumerable' . – tolanj Jan 27 '20 at 18:13

2 Answers2

4

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.

GSerg
  • 76,472
  • 17
  • 159
  • 346
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.

Servy
  • 202,030
  • 26
  • 332
  • 449
  • 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 ;-) – Baldewin Jan 28 '20 at 08:03