Question

Cela a devenu un peu long, alors voici la version rapide:

Pourquoi cela provoque-t-il une conduite d'exécution TypeLoadexception? (Et le compilateur devrait-il m'empêcher de le faire?)

interface I
{
    void Foo<T>();
}

class C<T1>
{
    public void Foo<T2>() where T2 : T1 { }
}

class D : C<System.Object>, I { } 

L'exception se produit si vous essayez d'instancier D.


Version plus longue et plus exploratoire:

Envisager:

interface I
{
    void Foo<T>();
}

class C<T1>
{
    public void Foo<T2>() where T2 : T1 { }
}

class some_other_class { }

class D : C<some_other_class>, I { } // compiler error CS0425

C'est illégal car les contraintes de type C.Foo() Ne correspond pas à ceux-ci sur I.Foo(). Il génère l'erreur du compilateur CS0425.

Mais je pensais que je pourrais être en mesure de enfreindre la règle:

class D : C<System.Object>, I { } // yep, it compiles

En utilisant Object En tant que contrainte sur T2, je suis nuisant cette contrainte. Je peux passer en toute sécurité n'importe quel type à D.Foo<T>(), parce que tout dérive de Object.

Même ainsi, je m'attendais toujours à obtenir une erreur de compilateur. Dans un C # Langue Sense, cela viole la règle selon laquelle "les contraintes sur c.foo () doivent correspondre aux contraintes sur i.foo ()", et j'ai pensé que le compilateur serait un stickler pour les règles. Mais il compile. Il semble que le compilateur voit ce que je fais, comprend qu'il est sûr et fera les yeux.

Je pensais que je m'en suis sorti, mais le temps d'exécution dit pas si vite. Si j'essaie de créer une instance de D, J'obtiens une typleloadexception: "Method 'c`1.foo' sur le type 'D' a essayé d'implémenter implicitement une méthode d'interface avec des contraintes de paramètres de type plus faible."

Mais cette erreur n'est-elle pas techniquement mal? N'utilise pas Object pour C<T1> nier la contrainte sur C.Foo(), ce qui le rend équivalent à - pas plus fort que - I.Foo()? Le compilateur semble être d'accord, mais l'exécution ne le fait pas.

Pour prouver mon point, je l'ai simplifié en prenant D hors de l'équation:

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class some_other_class { }

class C : I<some_other_class> // compiler error CS0425
{
    public void Foo<T>() { }
}

Mais:

class C : I<Object> // compiles
{
    public void Foo<T>() { }
}

Cela compile et fonctionne parfaitement pour tout type passé à Foo<T>().

Pourquoi? Y a-t-il un bug dans l'exécution, ou (plus probablement) y a-t-il une raison pour cette exception que je ne vois pas - dans ce cas, le compilateur n'aurait-il pas dû m'avoir arrêté?

Fait intéressant, si le scénario est inversé en déplaçant la contrainte de la classe à l'interface ...

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class C
{
    public void Foo<T>() { }
}

class some_other_class { }

class D : C, I<some_other_class> { } // compiler error CS0425, as expected

Et encore une fois, je nie la contrainte:

class D : C, I<System.Object> { } // compiles

Cette fois, ça fonctionne bien!

D d := new D();
d.Foo<Int32>();
d.Foo<String>();
d.Foo<Enum>();
d.Foo<IAppDomainSetup>();
d.Foo<InvalidCastException>();

Tout se passe, et cela me semble parfaitement logique. (Idem avec ou sans D dans l'équation)

Alors pourquoi la première façon se casse-t-elle?

Addenda:

J'ai oublié d'ajouter qu'il y a une solution de contournement simple pour la TypeLoadexception:

interface I
{
    void Foo<T>();
}

class C<T1>
{
    public void Foo<T2>() where T2 : T1 { }
}

class D : C<Object>, I 
{
    void I.Foo<T>() 
    {
        Foo<T>();
    }
}

Implémentation explicite I.Foo() c'est bien. Seule l'implémentation implicite provoque la typleloadexception. Maintenant je peux faire ceci:

        I d = new D();
        d.Foo<any_type_i_like>();

Mais c'est toujours un cas spécial. Essayez d'utiliser autre chose que System.Object, et cela ne se compile pas. Je me sens un peu sale de faire cela parce que je ne sais pas si cela fonctionne intentionnellement de cette façon.

Était-ce utile?

La solution

C'est un bug - voir La mise en œuvre de la méthode générique à partir de l'interface générique provoque TypeLoadexception et Code invérifiable avec interface générique et méthode générique avec contrainte de paramètre de type. Il n'est pas clair pour moi s'il s'agit d'un bug C # ou d'un bug CLR, cependant.

Ajouté par OP:

Voici ce que dit Microsoft dans le deuxième fil auquel vous avez lié (mon accent):

Il existe un décalage entre les algorithmes utilisés par l'exécution et le compilateur C # pour déterminer si un ensemble de contraintes est aussi fort qu'un autre ensemble. Cette inadéquation se traduit par le compilateur C # acceptant certaines constructions que le runtime rejette et le résultat est la typleloadexception que vous voyez. Nous enquêtons pour déterminer si ce code est une manifestation de ce problème. Quel que soit, Ce n'est certainement pas "par conception" que le compilateur accepte du code comme celui-ci Cela se traduit par une exception d'exécution.

Salutations,

Ed Maurer C # Lead de développement du compilateur

D'après la partie que j'ai en gras, je pense qu'il dit que c'est un bug du compilateur. C'était en 2007. Je suppose que ce n'est pas assez sérieux pour être une priorité pour eux de le réparer.

Autres conseils

La seule explication est que la contrainte est considérée comme faisant partie de la déclaration de la méthode. C'est pourquoi dans le premier cas, il s'agit d'une erreur de compilateur.

Le compilateur n'obtient pas l'erreur lorsque vous utilisez object... bien, c'est un bug du compilateur.

D'autres «contraintes» ont les mêmes propriétés de la contribution générique:

interface I
{
    object M();
}

class C
{
    public some_type M() { return null; }
}

class D : C, I
{
}

Je pourrais demander: pourquoi cela ne fonctionne pas?

Vous voyez? C'est tout à fait la même question que la vôtre. Il est parfaitement valable de mettre en œuvre object avec some_type, mais ni l'exécution, ni le compilateur ne l'accepteront.

Si vous essayez de générer du code MSIL et de forcer l'implémentation de mon exemple, l'exécution se plaindra.

L'implémentation implicite de l'interface a besoin que les contraintes génériques sur les déclarations de méthode soient équivalentes, mais pas nécessairement exactement les mêmes dans le code. De plus, les paramètres de type générique ont une contrainte implicite de "où t: objet". C'est pourquoi spécifier C<Object> Compiles, il fait que la contrainte devient équivalente à la contrainte implicite dans l'interface. (Section 13.4.3 du C # Spec linguistique).

Vous avez également raison que l'utilisation d'une implémentation d'interface explicite qui appelle votre méthode contrainte fonctionnera. Il fournit un mappage très clair de la méthode d'interface à votre implémentation dans la classe où les contraintes ne peux pas Différent, puis procède à l'appel d'une méthode générique nommée similaire (celle qui n'a plus rien à voir avec l'interface). À ce stade, les contraintes sur la méthode secondaire peuvent être résolues de la même manière que n'importe quel appel de méthode générique sans aucun problème de résolution d'interface.

Le déplacement des contraintes de la classe à l'interface, dans votre deuxième exemple, est meilleur car la classe prendra ses contraintes de l'interface par défaut. Cela signifie également que vous devez spécifier les contraintes de l'implémentation de votre classe, le cas échéant (et dans le cas de l'objet, il n'est pas applicable). Qui passe I<string> signifie que vous ne pouvez pas spécifier directement cette contrainte dans le code (car la chaîne est scellée) et qu'elle doit donc faire partie d'une implémentation d'interface explicite ou d'un type générique qui sera égal aux contraintes aux deux endroits.

Pour autant que je sache, l'exécution et le compilateur utilisent des systèmes de vérification distincts pour les contraintes. Le compilateur autorise ce cas mais le vérificateur d'exécution ne l'aime pas. Je veux souligner que je ne sais pas avec certitude pourquoi cela a un problème avec cela, mais je suppose que cela n'aime pas le potentiel de cette définition de classe ne pas remplissez les contraintes d'interface en fonction de ce que T finit par être réglé. Si quelqu'un d'autre a une réponse définitive à ce sujet, ce serait formidable.

En réponse à l'extrait basé sur l'interface:

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class C : I<string> // compiler error CS0425
{
    public void Foo<T>() { }
}

Je crois que le problème est que le compilateur reconnaît que:

  1. Vous n'avez pas déclaré les contraintes de type nécessaire sur c.foo ().
  2. Si vous choisissez String comme type, il n'y a pas de T valide sur c.foo () car un type ne peut pas hériter de la chaîne.

Pour voir ce travail dans la pratique, spécifiez une classe réelle qui pourrait être héritée de T1.

interface I<T1>
{
    void Foo<T2>() where T2 : T1;
}

class C : I<MyClass>
{
    public void Foo<T>() where T : MyClass { }
}

public class MyClass
{
}

Pour montrer que le chaîne de caractères le type n'est pas traité spécial en aucune façon ajouter simplement le scellé Mot-clé à la déclaration MyClass ci-dessus pour le voir échouer de la même manière si vous deviez spécifier T1 comme chaîne avec String que la contrainte de type sur C.Foo ().

public sealed class MyClass
{
}

En effet, la chaîne est scellée et ne peut pas former la base d'une contrainte.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top