Как сравнить два IEnumerable <T>в C#, если я не знаю фактический тип объекта? - PullRequest
4 голосов
/ 27 января 2020

Я борюсь с реализацией интерфейса IEquatable<> для класса. Класс имеет свойство Parameter, которое использует тип generi c. По сути, определение класса выглядит следующим образом:

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

    ...
}

В методе Equals() я использую EqualityComparer<T>.Default.Equals(Parameter, other.Parameter) для сравнения свойства. Как правило, это работает нормально - если свойство не является коллекцией, например IEnumerable<T>. Проблема заключается в том, что средство сравнения по умолчанию для IEnumerable<T> проверяет равенство ссылок.

Очевидно, что вы захотите использовать SequenceEqual() для сравнения IEnumerable<T>. Но чтобы это запустить, вам нужно указать тип c generi для метода SequenceEqual(). Это самое близкое, что я мог получить:

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

Но это не работает, потому что Convert.ChangeType() возвращает object. И, конечно, object не реализует SequenceEqual().

Как мне заставить это работать? Спасибо за любые советы!

С уважением, Оливер

Ответы [ 2 ]

3 голосов
/ 27 января 2020

Учитывая, что у вас есть универсальный c контейнер, с которым вы хотите сравнить различные универсальные c элементы, вы не хотите жестко программировать различные специфические c проверки на равенство для определенных типов. Будет много ситуаций, в которых сравнение на равенство по умолчанию не будет работать для того, что пытается сделать какой-то конкретный абонент. В комментариях есть множество различных примеров проблем, которые могут возникнуть, но также просто рассмотрим множество классов many , для которых равенство по умолчанию является эталонным сравнением, для которого кто-то может захотеть сравнение значений. Вы не можете иметь этот компаратор равенства, просто жесткий код в решении для всех этих типов.

Решение, конечно, простое. Пусть вызывающая сторона предоставляет собственную реализацию равенства, что в C# означает IEqualityComparer<T>. Ваш класс может стать:

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

    ...
}

И теперь по умолчанию компаратор по умолчанию будет использоваться для любого данного типа, но вызывающая сторона всегда может указать компаратор не по умолчанию для любого типа, который нуждается в другой семантике равенства.

2 голосов
/ 27 января 2020

Фактически вам нужен способ сказать

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

, где T - IEnumerable<U>, а U - динамическое c.

Не думаю, что вы можете сделайте это.

Если вы довольны каким-то боксом, вы можете использовать не универсальный c IEnumerable интерфейс:

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

Если вы не довольны боксом , затем вы можете использовать отражение для создания и вызова SequenceEqual (как вдохновлено Как вызвать метод расширения с использованием отражения? ):

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

Вы можете кэшировать sequenceEqualMethod для лучшей производительности.

...