Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 303
H Bellamy Avatar asked Jul 11 '17 12:07

H Bellamy


People also ask

What is IEnumerable T?

IEnumerable<T> contains a single method that you must implement when implementing this interface; GetEnumerator, which returns an IEnumerator<T> object. The returned IEnumerator<T> provides the ability to iterate through the collection by exposing a Current property.

Is generic type parameter?

Generic MethodsA type parameter, also known as a type variable, is an identifier that specifies a generic type name. The type parameters can be used to declare the return type and act as placeholders for the types of the arguments passed to the generic method, which are known as actual type arguments.

What is IEnumerable<> in c#?

IEnumerable is an interface defining a single method GetEnumerator() that returns an IEnumerator interface. It is the base interface for all non-generic collections that can be enumerated. This works for read-only access to a collection that implements that IEnumerable can be used with a foreach statement.

What is the return type of IEnumerable?

IEnumerable interface Returns an enumerator that iterates through the collection.


2 Answers

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);
            }
        }
    }
}
like image 94
Kirill Shlenskiy Avatar answered Oct 18 '22 01:10

Kirill Shlenskiy


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");
}
like image 1
InBetween Avatar answered Oct 18 '22 02:10

InBetween