web-dev-qa-db-fra.com

Pourquoi "ref" et "out" ne prennent-ils pas en charge le polymorphisme?

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 deux arguments ref et out.

119
Andreas Grech

=============

UPDATE: J'ai utilisé cette réponse comme base pour cette entrée de blog:

Pourquoi les paramètres ref et out ne permettent-ils pas la variation de type?

Voir la page du blog pour plus de commentaires sur cette question. Merci pour la bonne question.

=============

Supposons que vous avez des classes Animal, Mammal, Reptile, Giraffe, Turtle et Tiger, avec les relations évidentes de sous-classes.

Supposons maintenant que vous avez une méthode void M(ref Mammal m). M peut à la fois lire et écrire m


Pouvez-vous passer une variable de type Animal à M

Non. Cette variable peut contenir une Turtle, mais M supposera qu'elle ne contient que des mammifères. Une Turtle n'est pas une Mammal.

Les paramètres Conclusion 1: ref ne peuvent pas être "plus grands". (Il y a plus d'animaux que de mammifères, donc la variable devient "plus grande" car elle peut contenir plus de choses.)


Pouvez-vous passer une variable de type Giraffe à M

M peut écrire à m et M voudra peut-être écrire un Tiger dans m. Maintenant vous avez mis une Tiger dans une variable qui est en fait de type Giraffe.

Les paramètres Conclusion 2: ref ne peuvent pas être "plus petits".


Maintenant, considérons N(out Mammal n).

Pouvez-vous passer une variable de type Giraffe à N

N peut écrire à n et N voudra peut-être écrire un Tiger

Les paramètres Conclusion 3: out ne peuvent pas être "plus petits".


Pouvez-vous passer une variable de type Animal à N?

Hmm.

Eh bien pourquoi pas? N ne peut pas lire à partir de n, il ne peut que lui écrire, non? Vous écrivez une Tiger dans une variable de type Animal et vous êtes tous ensemble, 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 à n avant que N soit retourné normalement. (Si N jette, tous les paris sont off.)

2) N doit écrire quelque chose dans n avant de pouvoir lire quelque chose dans n.

Cela permet cette séquence d'événements:

  • Déclarez un champ x de type Animal.
  • Passez x en tant que paramètre out à N
  • N écrit une Tiger dans n, qui est un alias pour x
  • Sur un autre fil, quelqu'un écrit une Turtle dans x
  • N tente de lire le contenu de n et découvre une Turtle dans ce qu’elle considère être une variable de type Mammal.

Clairement, nous voulons rendre cela illégal. 

Les paramètres Conclusion 4: out ne peuvent pas être agrandis.


Conclusion finale: Ni les paramètres ref ni out ne peuvent en changer les types. Autrement, 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 .

161
Eric Lippert

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'assimiler a = new A (), cela sera invalide.
Même raison pour laquelle vous ne pouvez pas écrire:

B b = new A();
28
maciejkow

Vous vous débattez avec le classique OOP - problème de covariance (et de contravariance), voir wikipedia : bien que ce fait puisse défier les attentes intuitives, il est mathématiquement impossible de permettre la substitution de classes au lieu de celles de base pour les arguments mutables (assignables) (et aussi les conteneurs dont les éléments sont assignables, pour la même raison) tout en respectant le principe de Liskov . Pourquoi est-ce ainsi que cela est esquissé dans les réponses existantes et exploré plus en profondeur dans ces articles de wiki et leurs liens.

Les langages POO qui semblent le faire, tout en restant traditionnellement statiques, sont "tricheurs" (insertion de vérifications de type dynamiques cachées, ou nécessitant un examen au moment de la compilation de TOUTES LES sources à vérifier); le choix fondamental est le suivant: soit abandonner cette covariance et accepter le mystère des praticiens (comme C # l’a fait ici), soit adopter une approche de frappe dynamique (comme le tout premier OOP langage, Smalltalk, l’a fait), ou déplacez-vous vers les données immuables (affectation unique), comme le font les langages fonctionnels (sous Immutability, vous pouvez prendre en charge la covariance et éviter d'autres énigmes connexes, comme le fait que vous ne pouvez pas avoir la sous-classe Square Rectangle dans un monde de données mutables).

9
Alex Martelli

Considérer:

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

4
Henk Holterman

Parce que donner Foo2 un ref B donnerait un objet mal formé parce que Foo2 sait seulement comment remplir A une partie de B.

2
CannibalSmith

Alors que les autres réponses ont expliqué succinctement le raisonnement derrière ce comportement, je pense qu’il est utile de mentionner que si vous devez réellement 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 {}  
}
1
BrendanLoBuglio

Si vous utilisez des exemples pratiques pour vos types, vous le verrez:

SqlConnection connection = new SqlConnection();
Foo(ref connection);

Et maintenant vous avez votre fonction qui prend le ancestor (i.e.Object):

void Foo2(ref Object connection) { }

Qu'est-ce qui peut éventuellement être faux avec ça?

void Foo2(ref Object connection)
{
   connection = new Bitmap();
}

Vous venez juste d’attribuer une Bitmap à votre SqlConnection.

Ce n'est pas bon.


Essayez encore avec les autres:

SqlConnection conn = new SqlConnection();
Foo2(ref conn);

void Foo2(ref DbConnection connection)
{
    conn = new OracleConnection();
}

Vous avez rempli une OracleConnection over-top de votre SqlConnection.

0
Ian Boyd

N’est-ce pas le compilateur qui vous dit qu’il aimerait que vous convertissiez explicitement l’objet afin qu’il soit certain de connaître vos intentions?

Foo2(ref (A)b)
0
dlamblin

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 et 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 ont été trop longues

0
Shereef Marzouk

C’est logique du point de vue de la sécurité, mais j’aurais préféré cela si le compilateur donnait un avertissement au lieu d’une erreur, car il existe des utilisations légitimes des 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 ça marcherait?

0
Oofpez