Home > Net >  Casting of generic type T to List<> for list equality checking
Casting of generic type T to List<> for list equality checking

Time:10-05

I have a generic method Compare which receives two input parameters of type T in the signature and compares them for equality. A boolean is returned. The logic is fairly straight forward for 'ordinary' c# .net types.

However, the generic type T can also be a List<U> (e.g. List<int> xList and List<int> yList), which I would like to compare for equality. I do not know the type of U at compile time, but can obtain it dynamically as shown in the example below.

I'm trying to use Enumerable.SequenceEqual<V>(), for list comparison, but this method needs to know the type of at compile time. To circumnavigate this, I've tried to dynamically cast the T to List<U>, but without success. An error is thrown at runtime (indicated in the code snippet).

Is there an elegant way of doing a list comparison as attempted below, where the list internal type is not known at compile time? The closest I could come to an answer is here: How to use local variable as a type? Compiler says "it is a variable but is used like a type", and it seems awfully close to what I need, but I could not leverage this to a successful solution.

public bool Compare(T x, T y)
{
    bool isEqual;

    if (typeof(T).IsSubClassOfGenericBaseType(typeof(List<>)))
    {

        Type listElementType = 
            x.GetType().GetGenericArguments().Single();
        Type specificListType = (typeof(List<>).MakeGenericType(listElementType));
        dynamic xList = x.GetType().MakeGenericType(specificListType);  // <-- Exception is thrown here.
        dynamic yList = y.GetType().MakeGenericType(specificListType);

        isEqual = Enumerable.SequenceEqual(xList, yList);
    }
    else
    {
        isEqual = EqualityComparer<T>.Default.Equals(x, y);
    }            

    return isEqual;
}

The Type extension method IsSubClassOfGenericBaseType are as follows:

internal static bool IsSubClassOfGenericBaseType(
            this Type type, Type genericBaseType)
{
    if (   type.IsGenericType 
        && type.GetGenericTypeDefinition() == genericBaseType)
    {
        return true;
    }

    if (type.BaseType != null)
    {
        return IsSubClassOfGenericBaseType(
            type.BaseType, genericBaseType);
    }

    return false;
}

CodePudding user response:

Would something like this work for you?

public static bool Compare(object? x, object? y)
{
    if (ReferenceEquals(x, y))
        return true;

    if (x is null || y is null)
        return false;

    if (x is IEnumerable a && y is IEnumerable b)
        return a.Cast<object>().SequenceEqual(b.Cast<object>());

    return x.Equals(y);
}

Usage:

var a = new List<int> { 1, 2, 3 };
var b = new List<int> { 1, 2, 3 };
var c = new List<int> { 1, 2, 4 };
var d = new List<bool>{ true, false, true };
var e = "string";
var f = new Collection<int> { 1, 2, 3 };
var g = new List<short> { 1, 2, 3 };

Console.WriteLine(Compare(a, b)); // True
Console.WriteLine(Compare(a, c)); // False
Console.WriteLine(Compare(a, d)); // False
Console.WriteLine(Compare(a, e)); // False
Console.WriteLine(Compare(a, f)); // True - might not be what you want,
Console.WriteLine(Compare(a, g)); // False

Console.ReadLine();

Note that because I'm using SequenceEqual() (as per your suggestion) then the List<int> compares true to the equivalent Collection<int>. I think this is probably what you want, since a List<int> is a Collection<int>, but just so you are aware.

Also note that you shouldn't call the method Compare() in your code - it might cause people to confuse it with IComparer.Compare().

Do be aware that this solution is likely to be significantly slower than just doing:

public static bool CompareDynamic<T>(T x, T y)
{
    return typeof(T).IsSubClassOfGenericBaseType(typeof(List<>)) 
        ? Enumerable.SequenceEqual((dynamic)x, (dynamic)y) 
        : EqualityComparer<T>.Default.Equals(x, y);
}

This is due to all the casting (and hence boxing, if the elements are value types). If there are few elements, then it won't matter much but for large collections that could be a problem.


For completeness, here is some benchmark code that compares the performance of casting versus using dynamic for a fairly large int list:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Running;

namespace Demo
{
    [SimpleJob(RuntimeMoniker.Net50)]
    public class Program
    {
        List<int> a = Enumerable.Range(1, 100_000).ToList();
        List<int> b = Enumerable.Range(1, 100_000).ToList();

        static void Main()
        {
            BenchmarkRunner.Run<Program>();
            Console.ReadLine();
        }

        [Benchmark]
        public void ViaCompareCast()
        {
            CompareCast(a, b);
        }

        [Benchmark]
        public void ViaCompareDynamic()
        {
            CompareDynamic(a, b);
        }

        public static bool CompareCast(object? x, object? y)
        {
            if (ReferenceEquals(x, y))
                return true;

            if (x is null || y is null)
                return false;

            if (x is IEnumerable a && y is IEnumerable b)
                return a.Cast<object>().SequenceEqual(b.Cast<object>());

            return x.Equals(y);
        }

        public static bool CompareDynamic<T>(T x, T y)
        {
            return IsSubClassOfGenericBaseType(typeof(T), typeof(List<>)) 
                ? Enumerable.SequenceEqual((dynamic)x, (dynamic)y) 
                : EqualityComparer<T>.Default.Equals(x, y);
        }

        static bool IsSubClassOfGenericBaseType(Type type, Type genericBaseType)
        {
            if (type.IsGenericType && type.GetGenericTypeDefinition() == genericBaseType)
                return true;

            if (type.BaseType != null)
                return IsSubClassOfGenericBaseType(type.BaseType, genericBaseType);

            return false;
        }
    }
}

The results (for a .net 5.0 release build) are as follows:

|            Method |       Mean |    Error |    StdDev |     Median |
|------------------ |-----------:|---------:|----------:|-----------:|
|    ViaCompareCast | 4,930.6 us | 96.79 us | 191.04 us | 4,832.1 us |
| ViaCompareDynamic |   667.6 us | 12.67 us |  28.07 us |   652.7 us |

As you can see, the version using dynamic is more than 70 times faster for this particular data set. So you pays your money and you makes your choice!

  • Related