简体   繁体   中英

Generic type parameter to match anything that is IEnumerable<T>

I have the following C# code that does not behave as I would like.

The requirement is that anything that implements any IEnumerable<T> uses the second method that prints "2" , but anything else uses the first method that prints "1" .

A naive demonstration is below. ICollection<int> , IList<int> , List<int> and int[] all implement IEnumerable<T> but "1" is printed instead of "2"

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace Test
{
    public class Program
    {
        public static void Main()
        {
            var parent = new Parent<Class>();

            // OK: TProperty == int. Prints "1"
            parent.Map(c => c.IntValue);

            // OK: TProperty == int. Prints "2"
            parent.Map(c => c.IEnumerableIntValue);

            // Wrong: TProperty == ICollection<int>. Prints "1"
            parent.Map(c => c.ICollectionIntValue);

            // Wrong: TProperty == List<int>. Prints "1"
            parent.Map(c => c.ListIntValue);

            // Wrong: TProperty == int[]. Prints "1"
            parent.Map(c => c.ArrayIntValue);
        }

        public class Class
        {
            public int IntValue { get; set; }
            public IEnumerable<int> IEnumerableIntValue { get; set; }
            public ICollection<int> ICollectionIntValue { get; set; }
            public List<int> ListIntValue { get; set; }
            public int[] ArrayIntValue { get; set; }
        }
    }

    public class Parent<T>
    {
        public void Map<TProperty>(Expression<Func<T, TProperty>> expression)
        {
            Console.WriteLine("1");
        }

        public void Map<TProperty>(Expression<Func<T, IEnumerable<TProperty>>> expression)
        {
            Console.WriteLine("2");
        }
    }
}

I've tried changing the definition to

public void Map<TEnumerable, TElement>(Expression<Func<T, TEnumerable>> expression) where TEnumerable : IEnumerable<TElement>
{
    Console.WriteLine("2");
}

but this requires explicit type parameters to use, which is unacceptable:

parent.Map<int[], int>(c => c.ArrayIntValue);

Has anyone got an ideas on how to achieve this in C# at compile time? Any ideas are appreciated. Maybe contra/covariant delegates could work? I've tried wrangling with the C# compiler but have got nowhere.

Is it really that surprising that the only method whose type argument is unambiguously determined by the compiler to be IEnumerable<T> is one that actually deals with IEnumerable<T> explicitly?

Here's an unoptimised implementation which dynamically works out whether type TProperty unambiguously implements one (and only one) closed version of the IEnumerable<> interface, allowing you to process the expression tree differently in that particular case.

using System;
using System.Collections.Generic;
using System.Linq.Expressions;

namespace Test
{
    public class Program
    {
        public static void Main()
        {
            var parent = new Parent<Class>();

            // OK: TProperty == int. Prints "1"
            parent.Map(c => c.IntValue);

            // OK: TProperty == int. Prints "2"
            parent.Map(c => c.IEnumerableIntValue);

            // Wrong: TProperty == ICollection<int>. Prints "1"
            parent.Map(c => c.ICollectionIntValue);

            // Wrong: TProperty == List<int>. Prints "1"
            parent.Map(c => c.ListIntValue);

            // Wrong: TProperty == int[]. Prints "1"
            parent.Map(c => c.ArrayIntValue);
        }

        public class Class
        {
            public int IntValue { get; set; }
            public IEnumerable<int> IEnumerableIntValue { get; set; }
            public ICollection<int> ICollectionIntValue { get; set; }
            public List<int> ListIntValue { get; set; }
            public int[] ArrayIntValue { get; set; }
        }
    }

    public class Parent<T>
    {
        public void Map<TProperty>(Expression<Func<T, TProperty>> expression)
        {
            if (ReflectionHelpers.IsUnambiguousIEnumerableOfT(typeof(TProperty)))
            {
                MapMany(expression);
            }
            else
            {
                MapOne(expression);
            }
        }

        void MapOne(Expression expression)
        {
            Console.WriteLine("1");
        }

        void MapMany(Expression expression)
        {
            Console.WriteLine("2");
        }
    }

    static class ReflectionHelpers
    {
        public static bool IsUnambiguousIEnumerableOfT(Type type)
        {
            // Simple case - the type *is* IEnumerable<T>.
            if (IsIEnumerableOfT(type)) {
                return true;
            }

            // Harder - the type *implements* IEnumerable<T>.
            HashSet<Type> distinctIEnumerableImplementations = new HashSet<Type>();

            ExtractAllIEnumerableImplementations(type, distinctIEnumerableImplementations);

            switch (distinctIEnumerableImplementations.Count)
            {
                case 0: return false;
                case 1: return true;

                default:
                    // This may or may not be appropriate for your purposes.
                    throw new NotSupportedException("Multiple IEnumerable<> implementations detected.");
            }
        }

        private static bool IsIEnumerableOfT(Type type)
        {
            return type.IsGenericType
                && type.GetGenericTypeDefinition() == typeof(IEnumerable<>);
        }

        private static void ExtractAllIEnumerableImplementations(Type type, HashSet<Type> implementations)
        {
            foreach (Type interfaceType in type.GetInterfaces())
            {
                if (IsIEnumerableOfT(interfaceType)) {
                    implementations.Add(interfaceType);
                }

                ExtractAllIEnumerableImplementations(interfaceType, implementations);
            }
        }
    }
}

UPDATE My previous answer was downright wrong, didn't think it through properly.

No, you can't do it this way. The reason is that T will always be a better match than IEnumerable<T> for anything that isn't statically typed as an IEnumerable<T> , that's simply how generics work; there can't be a better generic match than T unless you have a contending exact match.

Consider the following:

void Foo<T>(T t) { }
void Foo<T>(IEquatable<T> equatable) { }

Would you actually expect Foo(1) to resolve to the second overload?

Or have Foo("hello") resolve to Foo<char>(IEnumerable<char>) when the applicable candidates are:

void Foo<T>(T t) { }
void Foo<T>(IEnumerable<T> enumerable) { }

The simplest solution is to make an explicit cast when mapping:

parent.Map(c => c.ICollectionIntValue.AsEnumerable());
parent.Map(c => c.ListIntValue.AsEnumerable());
//etc.

You could do something fancy mixing up some reflection with dynamic along the following lines:

public void Map<TProperty>(Expression<Func<T, TProperty>> expression)
{
    var genericInterfaces = typeof(TProperty).GetInterfaces().Where(i => i.IsGenericType);
    var iEnumerables = genericInterfaces.Where(i => i.GetGenericTypeDefinition().Equals(typeof(IEnumerable<>))).ToList();

    if (iEnumerables.Count > 1)
        throw new InvalidOperationException("Ambiguous IEnumerable<>");

    var iEnumerable = iEnumerables.FirstOrDefault();

    if (iEnumerable == null)
    {
        Console.WriteLine("1");
    }
    else
    {
        //ok, we know we have an IEnumerable of something. Let the runtime figure it out.
        Expression<Func<T, IEnumerable<dynamic>>> newExpression = e => expression.Compile()(e) as IEnumerable<dynamic>;
        Map(newExpression);
    }
}

public void Map<TProperty>(Expression<Func<T, IEnumerable<TProperty>>> expression)
{
    Console.WriteLine("2");
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM