如果我不知道实际的对象类型,如何在 C# 中比较两个 IEnumerable<T>?

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

我正在努力为 class 实现 IEquatable<> 接口。 class 有一个使用通用类型的 Parameter 属性。基本上 class 定义是这样的:

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>。但是要得到这个运行,需要指定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() returns 一个 object。当然 object 没有实现 SequenceEqual().

我如何让它工作?感谢您的提示!

此致, 奥利弗

实际上你想要一种表达方式

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

其中 TIEnumerable<U>U 是动态的。

我认为你做不到。

如果您喜欢一些拳击,您可以使用非通用 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(灵感来自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);
    }
}

您可以缓存 sequenceEqualMethod 以获得更好的性能。

鉴于您有一个要比较各种通用项的通用容器,您不希望在针对某些类型的各种特定相等性检查中进行硬编码。在 很多 的情况下,默认相等性比较对某些特定调用者尝试执行的操作不起作用。评论中有许多可能出现的不同问题示例,但也只考虑许多 many classes 那里的默认相等性是某人可能会参考的比较想要一个价值比较。您不能在所有这些类型的解决方案中硬编码这个相等比较器。

解决办法当然很简单。让调用者提供他们自己的相等性实现,在 C# 中,这意味着 IEqualityComparer<T>。你的 class 可以变成:

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

    ...
}

现在默认情况下,默认比较器将用于任何给定类型,但调用者始终可以为需要不同等式语义的任何类型指定非默认比较器。