Reputation: 22705
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.
Upvotes: 7
Views: 2676
Reputation: 32750
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");
}
Upvotes: 1
Reputation: 9587
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);
}
}
}
}
Upvotes: 2