Pourquoi «ref» et «out» ne prennent-ils pas en charge le polymorphisme?
-
05-07-2019 - |
Question
Prenez ce qui suit:
class A {}
class B : A {}
class C
{
C()
{
var b = new B();
Foo(b);
Foo2(ref b); // <= compile-time error:
// "The 'ref' argument doesn't match the parameter type"
}
void Foo(A a) {}
void Foo2(ref A a) {}
}
Pourquoi l’erreur de compilation ci-dessus se produit-elle? Cela se produit avec les arguments ref
et out
.
La solution
==============
UPDATE: j'ai utilisé cette réponse comme base pour cette entrée de blog:
Pourquoi les paramètres ref et out n'autorisent-ils pas la variation de type?
Voir la page du blog pour plus de commentaires sur cette question. Merci pour cette excellente question.
==============
Supposons que vous ayez les classes Animal
, Mammifère
, Reptile
, Giraffe
, Tortue
et Tiger
, avec les relations évidentes de sous-classes.
Supposons maintenant que vous avez une méthode void M (réf Mammal m)
. M
peut lire et écrire à la fois m
.
Pouvez-vous passer une variable de type
Animal
àM
?
Non. Cette variable pourrait contenir un Tortue
, mais M
supposera qu'elle ne contient que des mammifères. Un tortue
n'est pas un mammifère
.
Conclusion 1 : Les paramètres ref
ne peuvent pas être rendus "plus grands". (Il y a plus d'animaux que de mammifères, donc la variable devient "plus grosse" car elle peut contenir plus de choses.)
Pouvez-vous passer une variable de type
Giraffe
àM
?
Non. M
peut écrire dans m
et M
peut vouloir écrire un Tiger
dans m
. Vous avez maintenant ajouté un Tiger
dans une variable qui est en fait de type Giraffe
.
Conclusion 2 : Les paramètres ref
ne peuvent pas être rendus "plus petits".
Considérons maintenant N (out Mammal n)
.
Pouvez-vous passer une variable de type
Giraffe
àN
?
Non. N
peut écrire dans n
et N
peut vouloir écrire un Tiger
.
Conclusion 3 : Les paramètres out
ne peuvent pas être rendus "plus petits".
Pouvez-vous passer une variable de type
Animal
àN
?
Hmm.
Eh bien, pourquoi pas? N
ne peut pas lire dans n
, il ne peut y écrire que, n'est-ce pas? Vous écrivez un Tiger
dans une variable de type Animal
et vous êtes prêt, n'est-ce pas?
Faux. La règle n'est pas " N
ne peut écrire que dans n
".
Les règles sont, brièvement:
1) N
doit écrire dans n
avant que N
ne soit renvoyé normalement. (Si N
est lancé, tous les paris sont désactivés.)
2) N
doit écrire quelque chose sur n
avant de pouvoir lire quelque chose dans n
.
Cela permet cette séquence d'événements:
- Déclarez un champ
x
de typeAnimal
. - Transmettez
x
en tant que paramètreout
àN
. -
N
écrit unTiger
dansn
, qui est un alias pourx
. - Sur un autre sujet, quelqu'un écrit un
Tortue
dansx
. -
N
tente de lire le contenu den
et découvre uneTortue
dans ce qu'elle pense être une variable de typeMammal
.
Clairement, nous voulons rendre cela illégal.
Conclusion 4 : Les paramètres out
ne peuvent pas être rendus "plus grands".
Conclusion finale : Les paramètres ref
ni out
ne peuvent en modifier le type. Faire autrement, c'est casser la sécurité de type vérifiable.
Si ces questions vous intéressent dans la théorie des types de base, pensez à lire ma série sur la covariance et la contravariance en C # 4.0 .
Autres conseils
Parce que dans les deux cas, vous devez pouvoir affecter une valeur au paramètre ref / out.
Si vous essayez de passer b dans la méthode Foo2 en tant que référence et que vous essayez d'associer a = new A (), cela ne sera pas valide.
Même raison pour laquelle vous ne pouvez pas écrire:
B b = new A();
Vous vous débattez avec le problème classique de la POO de covariance (et de contravariance), voir wikipedia : bien que ce fait puisse défier les attentes intuitives, il est mathématiquement impossible d'autoriser la substitution de classes dérivées à la place de classes de base pour des arguments mutables (assignables) (et des conteneurs dont les éléments sont assignables, par exemple). pour la même raison) tout en respectant le principe de Liskov . La raison en est telle qu’elle est esquissée dans les réponses existantes et explorée plus en profondeur dans ces articles de wiki et leurs liens.
Les langues POO qui semblent le faire, tout en restant traditionnellement statiques, sont "triche". (insertion de vérifications de type dynamiques masquées ou nécessité d'un examen de TOUTES les sources à la compilation); le choix fondamental est le suivant: abandonner cette covariance et accepter l’énigme des praticiens (comme C # le fait ici), ou passer à une approche de frappe dynamique (comme le tout premier langage POO, Smalltalk, l’a fait), ou passer à une méthode immuable (simple). comme les langages fonctionnels (sous immutabilité, vous pouvez prendre en charge la covariance et éviter d’autres énigmes telles que le fait que vous ne pouvez pas avoir la sous-classe Square Rectangle dans un monde de données mutable).
Considérez:
class C : A {}
class B : A {}
void Foo2(ref A a) { a = new C(); }
B b = null;
Foo2(ref b);
Cela violerait la sécurité de type
Alors que les autres réponses ont expliqué de manière succincte le raisonnement derrière ce comportement, je pense qu’il est utile de mentionner que si vous devez vraiment faire quelque chose de ce genre, vous pouvez obtenir une fonctionnalité similaire en transformant Foo2 en une méthode générique, en tant que telle:
class A {}
class B : A {}
class C
{
C()
{
var b = new B();
Foo(b);
Foo2(ref b); // <= no compile error!
}
void Foo(A a) {}
void Foo2<AType> (ref AType a) where AType: A {}
}
Parce que donner à Foo2
un ref B
donnerait un objet mal formé, car Foo2
sait seulement comment remplir A
partie de B
.
N’est-ce pas le compilateur qui vous dit qu’il aimerait que vous convertissiez explicitement l’objet afin qu’il sache quelles sont vos intentions?
Foo2(ref (A)b)
Cela a du sens du point de vue de la sécurité, mais je l’aurais préféré si le compilateur donnait un avertissement au lieu d’une erreur, car il existe des utilisations légitimes d’objets polymorphes passés par référence. par exemple
class Derp : interfaceX
{
int somevalue=0; //specified that this class contains somevalue by interfaceX
public Derp(int val)
{
somevalue = val;
}
}
void Foo(ref object obj){
int result = (interfaceX)obj.somevalue;
//do stuff to result variable... in my case data access
obj = Activator.CreateInstance(obj.GetType(), result);
}
main()
{
Derp x = new Derp();
Foo(ref Derp);
}
Cela ne compilera pas, mais est-ce que cela fonctionnerait?
Si vous utilisez des exemples pratiques pour vos types, vous le verrez:
SqlConnection connection = new SqlConnection();
Foo(ref connection);
Et vous avez maintenant votre fonction qui prend l'ancêtre ( i.e. Objet
):
void Foo2(ref Object connection) { }
Qu'est-ce qui ne va peut-être pas avec ça?
void Foo2(ref Object connection)
{
connection = new Bitmap();
}
Vous venez juste d’attribuer un Bitmap
à votre SqlConnection
.
Ce n'est pas bon.
Réessayez avec les autres:
SqlConnection conn = new SqlConnection();
Foo2(ref conn);
void Foo2(ref DbConnection connection)
{
conn = new OracleConnection();
}
Vous avez bourré un OracleConnection
au-dessus de votre SqlConnection
.
Dans mon cas, ma fonction a accepté un objet et je ne pouvais rien envoyer alors j'ai tout simplement
object bla = myVar;
Foo(ref bla);
Et ça marche
Mon Foo est dans VB.NET. Il vérifie le type à l'intérieur et fait beaucoup de logique
Je m'excuse si ma réponse est en double mais que d'autres étaient trop longues