tutorial - Pourquoi le compilateur C#se plaint-il que "les types peuvent unifier" quand ils dérivent de différentes classes de base?




generic c# example (4)

Mon code actuel de non-compilation est similaire à ceci:

public abstract class A { }

public class B { }

public class C : A { }

public interface IFoo<T>
{
    void Handle(T item);
}

public class MyFoo<TA> : IFoo<TA>, IFoo<B>
    where TA : A
{
    public void Handle(TA a) { }
    public void Handle(B b) { }
}

Le compilateur C # refuse de compiler ceci, en citant la règle / erreur suivante:

'MyProject.MyFoo <TA>' ne peut pas implémenter à la fois 'MyProject.IFoo <TA>' et 'MyProject.IFoo <MyProject.B>' car ils peuvent être unifiés pour certaines substitutions de paramètres de type

Je comprends ce que cette erreur signifie; si TA pouvait être n'importe quoi, il pourrait aussi s'agir d'un B qui introduirait une ambiguïté sur les deux différentes implémentations de Handle .

Mais TA ne peut être rien. Basé sur la hiérarchie de type, TA ne peut pas être un B - au moins, je ne pense pas que ce soit possible. TA doit dériver de A , qui ne dérive pas de B , et évidemment il n'y a pas d'héritage de classe multiple dans C # / .NET.

Si je supprime le paramètre générique et remplace TA par C , ou même A , il compile.

Alors, pourquoi ai-je cette erreur? Est-ce un bug ou une non-intelligence générale du compilateur, ou y a-t-il autre chose qui me manque?

Est-ce qu'il y a une solution de contournement ou est-ce que je vais juste devoir ré-implémenter la classe générique de MyFoo tant que classe distincte non-générique pour chaque type dérivé TA éventuel?


Apparemment, il était de conception comme discuté à Microsoft Connect:

Et la solution de contournement est, définissez une autre interface comme:

public interface IIFoo<T> : IFoo<T>
{
}

Ensuite, implémentez ceci à la place:

public class MyFoo<TA> : IIFoo<TA>, IFoo<B>
    where TA : A
{
    public void Handle(TA a) { }
    public void Handle(B b) { }
}

Il compile maintenant bien, en mono .


Ceci est une conséquence de la section 13.4.2 de la spécification C # 4, qui stipule:

Si un type construit possible créé à partir de C, après que les arguments de type sont substitués dans L, fasse que deux interfaces dans L soient identiques, alors la déclaration de C est invalide. Les déclarations de contraintes ne sont pas prises en compte lors de la détermination de tous les types construits possibles.

Notez cette deuxième phrase là.

Ce n'est donc pas un bug dans le compilateur; le compilateur est correct. On pourrait soutenir que c'est une faille dans la spécification du langage.

D'une manière générale, les contraintes sont ignorées dans presque toutes les situations où un fait doit être déduit à propos d'un type générique. Les contraintes sont principalement utilisées pour déterminer la classe de base effective d'un paramètre de type générique, et rien d'autre.

Malheureusement, cela mène parfois à des situations où la langue est inutilement stricte, comme vous l'avez découvert.

C'est en général une mauvaise odeur de code d'implémenter deux fois la même interface, d'une certaine manière seulement distinguée par des arguments de type générique. Il est bizarre, par exemple, d'avoir la class C : IEnumerable<Turtle>, IEnumerable<Giraffe> - qu'est-ce que C c'est à la fois une séquence de tortues, et une séquence de girafes, en même temps ? Pouvez-vous décrire la chose que vous essayez de faire ici? Il pourrait y avoir un meilleur modèle pour résoudre le vrai problème.

Si en fait votre interface est exactement comme vous le décrivez:

interface IFoo<T>
{
    void Handle(T t);
}

Ensuite, l'héritage multiple de l'interface présente un autre problème. Vous pourriez raisonnablement décider de rendre cette interface contravariante:

interface IFoo<in T>
{
    void Handle(T t);
}

Supposons maintenant que vous avez

interface IABC {}
interface IDEF {}
interface IABCDEF : IABC, IDEF {}

Et

class Danger : IFoo<IABC>, IFoo<IDEF>
{
    void IFoo<IABC>.Handle(IABC x) {}
    void IFoo<IDEF>.Handle(IDEF x) {}
}

Et maintenant les choses deviennent vraiment folles ...

IFoo<IABCDEF> crazy = new Danger();
crazy.Handle(null);

Quelle implémentation de Handle s'appelle ???

Voir cet article et les commentaires pour plus de réflexions sur ce sujet:

http://blogs.msdn.com/b/ericlippert/archive/2007/11/09/covariance-and-contravariance-in-c-part-ten-dealing-with-ambiguity.aspx


Je sais que cela fait un moment que le fil de discussion a été posté mais pour ceux qui viennent à ce fil via le moteur de recherche d'aide. Notez que 'Base' représente la classe de base pour TA et B ci-dessous.

public class MyFoo<TA> : IFoo<Base> where TA : Base where B : Base
{
    public void Handle(Base obj) 
    { 
       if(obj is TA) { // TA specific codes or calls }
       else if(obj is B) { // B specific codes or calls }
    }

}

Prenant une supposition maintenant ...

A, B et C ne peuvent-ils pas être déclarés dans des assemblages extérieurs, où la hiérarchie des types peut changer après la compilation de MyFoo <T>, ce qui porte le chaos dans le monde?

La solution de contournement facile consiste simplement à implémenter Handle (A) au lieu de Handle (TA) (et utiliser IFoo <A> au lieu de IFoo <TA>). Vous ne pouvez pas faire beaucoup plus avec Handle (TA) que les méthodes d'accès de A (en raison de la contrainte A: TA) de toute façon.

public class MyFoo : IFoo<A>, IFoo<B> {
    public void Handle(A a) { }
    public void Handle(B b) { }
}






generics