2017-07-11 2 views
6

J'ai le code C# suivant qui ne se comporte pas comme je le voudrais.Paramètre de type générique pour correspondre à tout ce qui est IEnumerable <T>

L'exigence est que tout ce qui implémente IEnumerable<T> utilise la deuxième méthode qui imprime "2", mais tout le reste utilise la première méthode qui imprime "1".

Une démonstration naïve est ci-dessous. ICollection<int>, IList<int>, List<int> et int[] tout mettre en œuvre IEnumerable<T> mais "1" est imprimé à la place de "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"); 
     } 
    } 
} 

J'ai essayé de changer la définition de

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

mais cela nécessite des paramètres de type explicites à utiliser, ce qui est inacceptable:

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

Est-ce que quelqu'un a eu une idée sur la façon de réaliser cela en C# au moment de la compilation? Toutes les idées sont appréciées. Peut-être que les délégués contra/covariants pourraient travailler? J'ai essayé de me battre avec le compilateur C# mais je n'ai rien trouvé.

+3

. En utilisant vos attentes, tous les appels devraient imprimer "2". –

+0

Maintenant c'est un intéressant que je n'ai pas pensé à @rind –

+2

Que diriez-vous de créer la méthode séparée pour IEnumerable: 'public void MapEnumerable (Expression >> expression)' –

Répondre

1

MISE À JOUR Ma réponse précédente était carrément mauvais, ne pense pas à travers correctement.

Non, vous ne pouvez pas le faire de cette façon. La raison est que T sera toujours un meilleur match que IEnumerable<T> pour tout ce qui n'est pas statiquement typé comme IEnumerable<T>, c'est tout simplement comme ça que fonctionnent les génériques; il ne peut pas y avoir de meilleur concordance générique que T sauf si vous avez un concurrent.

Considérez ce qui suit:

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

Vous attendez-vous réellement Foo(1) à résoudre à la deuxième surcharge?

Ou ont Foo("hello") détermination à Foo<char>(IEnumerable<char>) lorsque les candidats applicables sont les suivants:

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

La solution la plus simple est de faire un casting explicite lorsque la cartographie:

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

Vous pourrait faire quelque chose mélange de fantaisie un peu de réflexion avec dynamic le long des lignes suivantes:

Btw: string implémente IEnumerable
2

Est-ce vraiment si surprenant que la seule méthode dont l'argument de type est déterminé sans ambiguïté par le compilateur soit IEnumerable<T> est celle qui traite réellement IEnumerable<T> explicitement?

Voici une implémentation non optimisée qui détermine dynamiquement si le type TProperty implémente sans ambiguïté une (et une seule) version fermée de l'interface IEnumerable<>, vous permettant de traiter l'arbre d'expression différemment dans ce cas particulier.

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); 
      } 
     } 
    } 
} 
+0

Je ne vois pas le problème du tout. L'inférence de type échoue simplement et vous devez spécifier explicitement les paramètres de type générique. – InBetween

+0

@InEntre, bon point. Ce n'était pas du tout un argument fort, j'ai donc choisi de le supprimer et de me concentrer sur la solution de contournement spécifique au scénario. –