43 votes

Aucun avertissement ou erreur (ou échec d'exécution) lorsque la contravariance entraîne une ambiguïté

Tout d'abord, rappelez-vous qu'un .NET String fois IConvertible et ICloneable.

Maintenant, considérez ce qui suit assez simple code:

//contravariance "in"
interface ICanEat<in T> where T : class
{
  void Eat(T food);
}

class HungryWolf : ICanEat<ICloneable>, ICanEat<IConvertible>
{
  public void Eat(IConvertible convertibleFood)
  {
    Console.WriteLine("This wolf ate your CONVERTIBLE object!");
  }

  public void Eat(ICloneable cloneableFood)
  {
    Console.WriteLine("This wolf ate your CLONEABLE object!");
  }
}

Ensuite, essayez les solutions suivantes (à l'intérieur d'une méthode):

ICanEat<string> wolf = new HungryWolf();
wolf.Eat("sheep");

Quand on compile ce, on obtient aucune erreur de compilation ou d'avertissement. Lors de l'exécution, elle ressemble à la méthode dépend de l'ordre de la liste d'interface dans ma class déclaration pour HungryWolf. (Essayez d'échanger les deux interfaces de la virgule (,) liste séparée.)

La question est simple: Ne pas donner au moment de la compilation avertissement (ou de le jeter au moment de l'exécution)?

Je ne suis probablement pas le premier à venir avec ce code. J'ai utilisé la contravariance de l'interface, mais vous pouvez faire un totalement analogue exemple avec covarainace de l'interface. Et, en fait, M. Lippert a fait juste que, il y a longtemps. Dans les commentaires de son blog, presque tout le monde convient qu'il doit être une erreur. Pourtant, ils permettent à ce silence. Pourquoi?

---

Étendue question:

Ci-dessus, nous avons exploité qu'une String fois Iconvertible (interface) et ICloneable (interface). Aucune de ces deux interfaces dérive de l'autre.

Maintenant voici un exemple avec une des classes de base qui est, en un sens, un peu pire.

Rappelez-vous qu'un StackOverflowException est à la fois un SystemException (classe de base directe) et un Exception (de la classe de base de la classe de base). Ensuite (si ICanEat<> , c'est comme avant):

class Wolf2 : ICanEat<Exception>, ICanEat<SystemException>  // also try reversing the interface order here
{
  public void Eat(SystemException systemExceptionFood)
  {
    Console.WriteLine("This wolf ate your SYSTEM EXCEPTION object!");
  }

  public void Eat(Exception exceptionFood)
  {
    Console.WriteLine("This wolf ate your EXCEPTION object!");
  }
}

Tester avec:

static void Main()
{
  var w2 = new Wolf2();
  w2.Eat(new StackOverflowException());          // OK, one overload is more "specific" than the other

  ICanEat<StackOverflowException> w2Soe = w2;    // Contravariance
  w2Soe.Eat(new StackOverflowException());       // Depends on interface order in Wolf2
}

Toujours pas d'avertissement, d'erreur ou d'exception. Dépend toujours de l'interface de l'ordre de la liste dans class déclaration. Mais la raison pourquoi je pense que c'est pire, c'est que, cette fois, quelqu'un pourrait penser que la résolution de surcharge serait toujours choisir SystemException parce que c'est plus précis que juste Exception.


État avant la bounty a été ouvert: Trois réponses de deux utilisateurs.

Statut sur le dernier jour de la bounty: Toujours pas de nouvelles réponses reçues. Si pas de réponses, se montrer, je dois d'attribution de la prime de Musulman Ben Dhaou.

9voto

jam40jeff Points 1464

Je crois que le compilateur ne la meilleure chose à VB.NET avec l'avertissement, mais je ne pense toujours pas que c'est aller assez loin. Malheureusement, la "bonne chose" probablement besoin interdisant quelque chose qui est potentiellement utiles(mise en place de la même interface avec deux covariant et contravariant paramètres de type générique) ou de l'introduction de quelque chose de nouveau à la langue.

Comme il est, il n'y a pas de place le compilateur pourrait affecter une erreur que l' HungryWolf classe. C'est le moment où une classe est prétendant à savoir comment faire quelque chose qui pourrait potentiellement être ambiguë. C'est en indiquant

Je sais comment manger un ICloneable, ou quoi que ce soit la mise en œuvre ou d'hériter d'elle, d'une certaine façon.

Et, je sais aussi comment manger un IConvertible, ou quoi que ce soit la mise en œuvre ou d'hériter d'elle, d'une certaine façon.

Cependant, il n'affirme jamais ce qu'il devrait faire s'il reçoit sur son plateau quelque chose qui est à la fois un ICloneable et IConvertible. Cela ne cause pas le compilateur toute la douleur si elle est donnée une instance d' HungryWolf, car il peut dire avec certitude "Hey, je ne sais pas quoi faire!". Mais il donnera le compilateur de la douleur quand elle est donnée à l' ICanEat<string> de l'instance. Le compilateur n'a aucune idée de ce que le type réel de l'objet dans la variable est, seulement qu'il n'a certainement mettre en oeuvre ICanEat<string>.

Malheureusement, quand un HungryWolf est stockée dans cette variable, il met en œuvre de manière ambiguë que même interface que deux fois. Alors certes, nous ne pouvons pas jeter une erreur lors de l'appel d' ICanEat<string>.Eat(string), car cette méthode existe et serait parfaitement valable pour de nombreux autres objets qui pourraient être placés dans l' ICanEat<string> variable (batwad déjà mentionné dans une de ses réponses).

En outre, bien que le compilateur pourrait se plaindre que la cession d'un HungryWolf objet à un ICanEat<string> variable est ambigu, il ne peut pas l'empêcher de se produire en deux étapes. Un HungryWolf peuvent être attribués à un ICanEat<IConvertible> variable, qui peut être transmise à d'autres méthodes et affectés dans un ICanEat<string> variable. Ces deux sont parfaitement légales affectations et il serait impossible pour le compilateur pour se plaindre de l'un des deux.

Ainsi, la première option consiste à interdire l' HungryWolf classe à partir de la mise en œuvre de deux ICanEat<IConvertible> et ICanEat<ICloneable> lorsque ICanEats'paramètre de type générique est contravariant, puisque ces deux interfaces pourrait unifier. Toutefois, cela supprime la capacité de code quelque chose d'utile avec aucune autre solution.

Option deux, malheureusement, aurait besoin d'un changement pour le compilateur, tant à l'IL et de la CLR. Cela permettrait à l' HungryWolf classe pour implémenter deux interfaces, mais il faudrait aussi que la mise en œuvre de l'interface ICanEat<IConvertible & ICloneable> interface, où le paramètre de type générique implémente deux interfaces. Ce n'est probablement pas le meilleur de la syntaxe(ce qui ne la signature de la présente Eat(T) méthode ressembler, Eat(IConvertible & ICloneable food)?). Probablement, une meilleure solution serait d'une auto-généré de type générique, sur la mise en œuvre de la classe, de sorte que la définition de la classe serait quelque chose comme:

class HungryWolf:
    ICanEat<ICloneable>, 
    ICanEat<IConvertible>, 
    ICanEat<TGenerated_ICloneable_IConvertible>
        where TGenerated_ICloneable_IConvertible: IConvertible, ICloneable {
    // implementation
}

IL faudrait alors que changé, pour être en mesure de permettre l'implémentation de l'interface types de être construits comme des classes génériques pour un callvirt enseignement:

.class auto ansi nested private beforefieldinit HungryWolf 
    extends 
        [mscorlib]System.Object
    implements 
        class NamespaceOfApp.Program/ICanEat`1<class [mscorlib]System.ICloneable>,
        class NamespaceOfApp.Program/ICanEat`1<class [mscorlib]System.IConvertible>,
        class NamespaceOfApp.Program/ICanEat`1<class ([mscorlib]System.IConvertible, [mscorlib]System.ICloneable>)!TGenerated_ICloneable_IConvertible>

Le CLR aurait alors de processus d' callvirt instructions par la construction d'une interface de mise en œuvre pour l' HungryWolf avec string que le paramètre de type générique pour TGenerated_ICloneable_IConvertible, et de vérifier pour voir si elle correspond mieux que les autres implémentations d'interface.

Pour la covariance, tout cela serait plus simple, car les interfaces supplémentaires doivent être mises en œuvre n'ont pas à être des paramètres de type générique avec des contraintes , mais simplement la plupart des dérivés de type de base entre les deux autres types, ce qui est connu au moment de la compilation.

Si la même interface est mis en œuvre plus de deux fois, puis le nombre d'interfaces supplémentaires doivent être mises en œuvre croît de façon exponentielle, mais ce serait le coût de la flexibilité et de sécurité du type de mise en œuvre de plusieurs contravariant(ou covariants) sur une seule classe.

Je doute que cela va le faire dans le cadre, mais ce serait ma solution préférée, surtout depuis la nouvelle complexité du langage serait toujours autonome de la classe qui le souhaite de ce qui est actuellement dangereux.


edit:
Grâce Jeppe pour me rappeler que la covariance est pas plus simple que la contravariance, en raison du fait que les interfaces communes doivent également être prises en compte. Dans le cas d' string et char[], l'ensemble des plus grands points communs serait {object, ICloneable, IEnumerable<char>} (IEnumerable est couvert par IEnumerable<char>).

Toutefois, cela nécessiterait une nouvelle syntaxe de l'interface paramètre de type générique contrainte, pour indiquer que le paramètre de type générique ne doit

  • hériter de la classe spécifiée ou une classe de mise en œuvre au moins une des interfaces spécifiées
  • mettre en œuvre au moins une des interfaces spécifiées

Peut-être quelque chose comme:

interface ICanReturn<out T> where T: class {
}

class ReturnStringsOrCharArrays: 
    ICanReturn<string>, 
    ICanReturn<char[]>, 
    ICanReturn<TGenerated_String_ArrayOfChar>
        where TGenerated_String_ArrayOfChar: object|ICloneable|IEnumerable<char> {
}

Le paramètre de type générique TGenerated_String_ArrayOfChar dans ce cas(où un ou plusieurs interfaces sont communes) doivent toujours être traités comme des object, même si la classe de base commune a déjà dérivé d' object; parce que le type le plus commun peut mettre en œuvre une interface commune, sans hériter de la classe de base commune.

6voto

Moslem Ben Dhaou Points 2271

Une erreur de compilateur n'a pas pu être générée dans de tels cas, parce que le code est correct et il devrait fonctionner correctement avec tous les types qui n'hérite pas tant à l'interne qu'au même moment. Le problème est le même si vous hériter d'une classe et une interface en même temps. (c'est à dire l'utilisation de la base object de la classe dans votre code).

Pour une raison quelconque VB.Net le compilateur va lancer un avertissement dans de tels cas similaire à

L'Interface " ICustom(De Foo) est ambigu avec une autre interface implémentée 'ICustom(De Boo)" en raison du " In " et " Out " paramètres dans l'Interface de ICustom(En T)"

Je suis d'accord que le compilateur C# doit lancer une alerte semblable ainsi. Cochez cette Débordement de Pile question. M. Lippert a confirmé que le moteur d'exécution en choisir un et que ce genre de programmation doit être évitée.

4voto

batwad Points 1559

Voici une tentative de justification de l'absence d'avertissement ou d'erreur que je suis juste venu avec, et je n'ai même pas bu!

Résumé de votre code, ce qui est l'intention?

ICanEat<string> beast = SomeFactory.CreateRavenousBeast();
beast.Eat("sheep");

Vous êtes nourrir quelque chose. Comment la bête en fait il mange jusqu'à la bête. Si il a été donné de la soupe, il peut décider d'utiliser une cuillère. Il peut utiliser un couteau et une fourchette pour manger les moutons. Il pourrait déchirer en lambeaux avec ses dents. Mais le point est que l'appelant de cette catégorie, nous ne se soucient pas comment il mange, nous voulons juste d'être nourries.

En définitive, c'est de la bête de décider de la façon de manger ce qu'il est donné. Si la bête décide de manger un ICloneable et IConvertible c'est ensuite à la bête pour comprendre comment gérer les deux cas. Pourquoi le garde-chasse de soins de comment les animaux mangent leur repas?

Si elle n'a causé une erreur de compilation de vous faire de la mise en œuvre d'une nouvelle interface d'une modification de rupture. Par exemple, si vous expédiez HungryWolf comme ICanEat<ICloneable> puis des mois plus tard, ajouter ICanEat<IConvertible> vous cassez chaque endroit où elle a été utilisée avec un type qui implémente deux interfaces.

Et il coupe dans les deux sens. Si l'argument générique n'implémente ICloneable il travail tout à fait heureux avec le loup. Les Tests passent, navire, je vous remercie beaucoup, Monsieur le Loup. L'année prochaine vous ajoutez IConvertible soutien à votre type et BANG! Pas très utile une erreur, vraiment.

2voto

batwad Points 1559

Une autre explication: il n'y a pas d'ambiguïté, il n'est donc pas d'avertissement du compilateur.

Ours avec moi.

ICanEat<string> wolf = new HungryWolf();
wolf.Eat("sheep");

Il n'y a pas d'erreur, car ICanEat<T> seulement contient une méthode appelée Eat avec ces paramètres. Mais dire cela à la place et vous ne obtenez un message d'erreur:

HungryWolf wolf = new HungryWolf();
wolf.Eat("sheep");

HungryWolf est ambigu, pas ICanEat<T>. En attendant une erreur de compilation, vous demandez au compilateur de regarder la valeur de la variable au point d' Eat est appelé et si elle est ambiguë, dont il n'a pas nécessairement quelque chose qui peut elle peut en déduire. Considérons une classe UnambiguousCow qui n'implémente ICanEat<string>.

ICanEat<string> beast;

if (somethingUnpredictable)
    beast = new HungryWolf();
else
    beast = new UnambiguousCow();

beast.Eat("dinner");

Où et comment vous attendez-vous à une erreur de compilateur être soulevées?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X