2009-05-28 11 views
6

J'ai rencontré un problème de compilation aujourd'hui qui m'a déconcerté. Considérez ces deux classes de conteneur.Génériques, héritage et résolution de méthode échouée du compilateur C#

public class BaseContainer<T> : IEnumerable<T> 
{ 
    public void DoStuff(T item) { throw new NotImplementedException(); } 

    public IEnumerator<T> GetEnumerator() { } 
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { } 
} 
public class Container<T> : BaseContainer<T> 
{ 
    public void DoStuff(IEnumerable<T> collection) { } 

    public void DoStuff <Tother>(IEnumerable<Tother> collection) 
     where Tother: T 
    { 
    } 
} 

Les anciens et les DoStuff(T item) définit celui-ci avec le surcharge DoStuff <Tother>(IEnumerable<Tother>) spécifiquement pour contourner l'absence de covariance/contravariance de C# (jusqu'à 4 je l'entends).

Ce code

Container<string> c = new Container<string>(); 
c.DoStuff("Hello World"); 

frappe une erreur de compilation assez étrange. Notez l'absence de <char> à partir de l'appel de méthode.

Le type 'char' ne peut pas être utilisé comme paramètre de type 'Tautre' dans le type générique ou la méthode 'Container.DoStuff (System.Collections.Generic.IEnumerable). Il n'y a pas de conversion de boxe de 'char' à 'string'.

Essentiellement, le compilateur essaie de bloquer mon appel à DoStuff(string) en Container.DoStuff<char>(IEnumerable<char>) parce que string met en œuvre IEnumerable<char>, plutôt que d'utiliser BaseContainer.DoStuff(string).

La seule façon que j'ai trouvé pour faire de cette compilation est d'ajouter DoStuff(T) à la classe dérivée

public class Container<T> : BaseContainer<T> 
{ 
    public new void DoStuff(T item) { base.DoStuff(item); } 

    public void DoStuff(IEnumerable<T> collection) { } 

    public void DoStuff <Tother>(IEnumerable<Tother> collection) 
     where Tother: T 
    { 
    } 
} 

Pourquoi le compilateur tente de bloquer une chaîne comme IEnumerable<char> quand 1), il sait qu'il peut » t (étant donné la présence d'une erreur de compilation) et 2) il a une méthode dans la classe de base qui compile bien? Ai-je mal compris quelque chose sur les génériques ou les trucs de méthode virtuelle en C#? Y a-t-il une autre solution autre que l'ajout d'un new DoStuff(T item) à Container?

+3

Je suis d'accord, cela semble bizarre, mais il est correct selon les spécifications. Ceci est une conséquence de l'interaction de deux règles: (1) la vérification de l'applicabilité de la résolution de surcharge survient AVANT la vérification des contraintes, et (2) les méthodes applicables dans les classes dérivées sont TOUJOURS meilleures que les méthodes applicables dans les classes de base. Les deux sont des règles raisonnablement raisonnables; ils arrivent juste à interagir particulièrement mal dans votre cas. –

+1

Pour plus de détails, voir section 7.5.5.1, en particulier les bits qui disent: (1) "Si la meilleure méthode est une méthode générique, les arguments de type (fournis ou inférés) sont vérifiés par rapport aux contraintes ..." et (2) " l'ensemble des méthodes candidates est réduit pour ne contenir que des méthodes provenant des types les plus dérivés ... " –

+2

Finalement, votre problème ici est un problème de conception. Vous surchargez une méthode "DoStuff" pour signifier à la fois "faire des choses à une seule valeur de type T", et "faire des choses à une séquence de valeurs de type T". Cela se heurte à de sérieux problèmes de "résolution d'intention" de plusieurs façons - par exemple, lorsque "type T" est lui-même une séquence. Vous constaterez que les classes de collection existantes dans la BCL ont été soigneusement conçues pour éviter ce problème; les méthodes qui prennent un élément sont appelées "Frob", les méthodes qui prennent une séquence d'éléments sont appelées "FrobRange", par exemple "Add" et "AddRange" dans les listes. –

Répondre

3

Modifier

Ok ... Je pense que je vois votre confusion maintenant. Vous vous êtes attendu à ce que DoStuff (chaîne) ait gardé le paramètre sous forme de chaîne et ait parcouru la liste des méthodes BaseClass en recherchant d'abord une signature appropriée, et en échouant cette tentative de conversion du paramètre en un autre type.

Mais il est arrivé l'inverse ... Au lieu Container.DoStuff(string) est allé, meh "theres une méthode de classe de base là-bas qui correspond à la facture, mais je vais convertir en un IEnumerable et avoir une crise cardiaque sur ce qui est disponible dans la classe actuelle à la place ...

Hmmm ... Je suis sûr que Jon ou Marc seraient en mesure de carillon à ce point au paragraphe spécifique C# Spec couvre ce coin cas

originale

Les deux méthodes attendent un Col IEnumerable lection

Vous passez une chaîne individuelle.

Le compilateur prend cette chaîne et va,

Ok, j'ai une chaîne, deux méthodes attendre un IEnumerable<T>, donc je vais transformer cette chaîne en un IEnumerable<char> ... Fait

droite, cochez la première méthode ... hmmm ... cette classe est un Container<string> mais j'ai une IEnumerable<char> si ça ne va pas.

Vérifiez la deuxième méthode, hmmm .... Je ont un IEnumerable<char> mais ombles n'implémente pas la chaîne de sorte que ce ne va pas non plus.

COMPILER erreur

Alors, que # s le correctif, bien que cela dépend complètement ce que vous essayez d'atteindre ... les deux éléments suivants seraient valables, pour l'essentiel, vos types d'utilisation est juste incorrect dans votre incarnation.

 Container<char> c1 = new Container<char>(); 
     c1.DoStuff("Hello World"); 

     Container<string> c2 = new Container<string>(); 
     c2.DoStuff(new List<string>() { "Hello", "World" }); 
+0

Donc le compilateur choisit une surcharge uniquement par les types de paramètres et non par les contraintes qui leur sont imposées (pas comme un ensemble de paramètres + contraintes)? Cela semble plutôt faible et à moitié cuit. Il * peut * trouver une méthode qu'il * peut * utiliser mais c'est opter * pas * pour. –

+0

non il ne peut pas ... aucune de vos méthodes satisfont le type de votre passage po Conteneur peut actuellement exécuter seulement un DoStuff (IEnumerable ) OU DoStuff (IEnumerable ) <- ce qui est rien puisque la chaîne est un classe scellée. –

+0

Ça ne peut pas? Qu'est-il arrivé à BaseContainer.DoStuff (T)? En ce qui concerne votre correction modifier. Je veux un conteneur de chaîne et la chaîne "Hello World" à DoStuff'ed. Si je ne peux pas réparer la classe alors DoStuff (new string [] {"Hello World"}); est ce que je veux, mais c'est une API vraiment minable à fournir (je pense). –

1

Je pense que cela a quelque chose à voir avec le fait que char est un type de valeur et que string est un type de référence. On dirait que vous définissez

TOther : T 

et le charbon ne dérive pas de chaîne.

+0

Par conséquent mon ahurissement pourquoi il choisit DoStuff (IEnumerable ) comme la méthode que j'appelle. Je ne spécifie pas DoStuff n'est pas char dérivé de la chaîne. Cela n'a aucun sens. –

+0

Je me demande si la chaîne peut être convertie en IEnumerable et le compilateur infère que Tother doit être char? – n8wrl

+0

Je pense que c'est exactement ce qu'il fait mais je n'appelle pas DoStuff , juste DoStuff. Je n'ai jamais vu le générique supposé comme ça. –

2

Le compilateur essaie de faire correspondre le paramètre à IEnumerable <T>. Le type String implémente IEnumerable <char>, donc il suppose que T est "char". Après cela, le compilateur vérifie l'autre condition "où OtherT: T", et cette condition n'est pas remplie. D'où l'erreur du compilateur.

2

Mon GUESS, et c'est une supposition parce que je ne sais pas vraiment, c'est qu'il regarde d'abord dans la classe dérivée pour résoudre l'appel de la méthode (parce que votre objet est celui du type dérivé). Si, et seulement s'il ne le peut pas, il passe à l'examen des méthodes des classes de base pour le résoudre. Dans votre cas, comme il peut résoudre le problème en utilisant la surcharge, il a essayé de le bloquer dans cela. Donc, il peut résoudre le problème en ce qui concerne le paramètre, mais il se heurte à un problème sur les contraintes. À ce stade, il est déjà résolu votre surcharge, donc il ne regarde pas plus loin, mais renvoie juste une erreur. Avoir du sens?

+0

Excepté qu'il suppose implicitement Tother = char indépendamment du fait que je ne spécifie pas cela. Je n'ai jamais vu le générique sur une méthode simplement supposé parce qu'il peut entasser l'argument en un. –

+1

N'avez-vous jamais utilisé LINQ? La plus grande partie de LINQ est construite sur l'idée que le paramètre type peut être supposé si le type est fourni sur l'un des paramètres de la méthode. –

+2

Vous spécifiez que Tother est char, en passant dans un IEnumerable , alias "string". Vous n'êtes peut-être pas familiarisé avec la fonctionnalité "inférence de type de méthode" de C#, mais cela existe depuis C# 2.0. Voir la section "inférence de type" de mon blog si vous voulez des détails sur le fonctionnement des algorithmes d'inférence de type de méthode; ils sont assez fascinants. –

3

Comme Eric Lippert a expliqué, le compilateur choisit la méthode DoStuff<Tother>(IEnumerable<Tother>) where Tother : T {} parce qu'il choisit les méthodes avant de vérifier les contraintes. Puisque la chaîne peut faire IEnumerable<>, le compilateur l'associe à cette méthode de classe enfant. Le compilateur fonctionne correctement comme décrit dans la spécification C#.

L'ordre de résolution de méthode souhaité peut être forcé en implémentant DoStuff en tant que extension method. Les méthodes d'extension sont vérifiées après méthodes de classe de base, donc il ne sera pas essayer de faire correspondre string contre l » IEnumerable<Tother>DoStuff jusqu'à après il a essayé de faire correspondre contre DoStuff<T>.

Le code suivant illustre l'ordre, la covariance et l'héritage de résolution de méthode souhaités. Veuillez le copier/coller dans un nouveau projet.

Ce plus grand inconvénient je peux penser est si loin que vous ne pouvez pas utiliser base dans les méthodes prépondérants, mais je pense qu'il ya des façons de contourner cela (vous demander si vous êtes intéressé).

using System; 
using System.Collections.Generic; 

namespace MethodResolutionExploit 
{ 
    public class BaseContainer<T> : IEnumerable<T> 
    { 
     public void DoStuff(T item) { Console.WriteLine("\tbase"); } 
     public IEnumerator<T> GetEnumerator() { return null; } 
     System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() { return null; } 
    }   
    public class Container<T> : BaseContainer<T> { } 
    public class ContainerChild<T> : Container<T> { } 
    public class ContainerChildWithOverride<T> : Container<T> { } 
    public static class ContainerExtension 
    { 
     public static void DoStuff<T, Tother>(this Container<T> container, IEnumerable<Tother> collection) where Tother : T 
     { 
      Console.WriteLine("\tContainer.DoStuff<Tother>()"); 
     } 
     public static void DoStuff<T, Tother>(this ContainerChildWithOverride<T> container, IEnumerable<Tother> collection) where Tother : T 
     { 
      Console.WriteLine("\tContainerChildWithOverride.DoStuff<Tother>()"); 
     } 
    } 

    class someBase { } 
    class someChild : someBase { } 
    class Program 
    { 
     static void Main(string[] args) 
     { 
      Console.WriteLine("BaseContainer:"); 
      var baseContainer = new BaseContainer<string>(); 
      baseContainer.DoStuff(""); 

      Console.WriteLine("Container:"); 
      var container = new Container<string>(); 
      container.DoStuff(""); 
      container.DoStuff(new List<string>()); 

      Console.WriteLine("ContainerChild:"); 
      var child = new ContainerChild<string>(); 
      child.DoStuff(""); 
      child.DoStuff(new List<string>()); 

      Console.WriteLine("ContainerChildWithOverride:"); 
      var childWithOverride = new ContainerChildWithOverride<string>(); 
      childWithOverride.DoStuff(""); 
      childWithOverride.DoStuff(new List<string>()); 

      //note covariance 
      Console.WriteLine("Covariance Example:"); 
      var covariantExample = new Container<someBase>(); 
      var covariantParameter = new Container<someChild>(); 
      covariantExample.DoStuff(covariantParameter); 

      // this won't work though :(
      // var covariantExample = new Container<Container<someBase>>(); 
      // var covariantParameter = new Container<Container<someChild>>(); 
      // covariantExample.DoStuff(covariantParameter); 

      Console.ReadKey(); 
     } 
    } 
} 

est ici la sortie:

BaseContainer: 
     base 
Container: 
     base 
     Container.DoStuff<Tother>() 
ContainerChild: 
     base 
     Container.DoStuff<Tother>() 
ContainerChildWithOverride: 
     base 
     ContainerChildWithOverride.DoStuff<Tother>() 
Covariance Example: 
     Container.DoStuff<Tother>() 

Pouvez-vous voir des problèmes avec ce travail autour?

+0

Hmm, idée intéressante. Je n'ai pas mis ces surcharges sur BaseContainer parce que j'essaie de garder cette classe très basique. Je pourrais facilement mettre DoStuff sur BaseContainer mais ce genre de chose va à l'encontre de mon objectif de garder BaseContainer basique. Extension signifie que techniquement, je garde BaseContainer comme je le veux mais ... :) –

+0

Colin, il * ne * met pas les surcharges sur le BaseContainer. c'est-à-dire qu'il ne détruit pas l'intellisense de BaseContainer. Essayez-le s'il vous plaît. Si j'ai mal compris, veuillez clarifier. – dss539

+0

Mes excuses, j'ai mal interprété le prototype de méthode. L'utilisation d'une extension sur une classe générique, cependant, signifie que je dois invoquer c.DoStuff au lieu de juste c.DoStuff , non? –

0

Je ne suis pas vraiment clair sur ce que vous essayez d'accomplir, ce qui vous empêche de simplement en utilisant deux méthodes, DoStuff(T item) and DoStuff(IEnumerable<T> collection)?

+0

Le problème est lorsque T lui-même est un IEnumerable depuis le compilateur se verrouille sur DoStuff (IEnumerable ), pas DoStuff (T). –