Pourquoi «ref» et «out» ne supportent-ils pas le polymorphisme?

124

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 à la fois refet outarguments.

Andreas Grech
la source

Réponses:

169

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

MISE À JOUR: 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?

Consultez la page du blog pour plus de commentaires sur ce problème. Merci pour la bonne question.

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

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

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


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

Non. Cette variable peut contenir un Turtle, mais Msupposera qu'elle ne contient que des mammifères. A Turtlen'est pas un Mammal.

Conclusion 1 : les refparamètres ne peuvent pas être «agrandis». (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. MPeut écrire met Msouhaitera peut-être écrire un Tigerdans m. Vous avez maintenant mis a Tigerdans une variable qui est en fait de type Giraffe.

Conclusion 2 : les refparamètres ne peuvent pas être rendus "plus petits".


Considérez maintenant N(out Mammal n).

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

Non. NPeut écrire net Nsouhaitera peut-être écrire un fichier Tiger.

Conclusion 3 : les outparamètres ne peuvent pas être «plus petits».


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

Hmm.

Eh bien pourquoi pas? Nne peut pas lire n, il ne peut qu'écrire, non? Vous écrivez un Tigerdans une variable de type Animalet vous êtes tous ensemble, non?

Faux. La règle n'est pas " Npeut seulement écrire n".

Les règles sont, brièvement:

1) Ndoit écrire navant de Nretourner normalement. (En cas de Nlancers, tous les paris sont ouverts.)

2) Ndoit écrire quelque chose dans navant de lire quelque chose à partir de n.

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

  • Déclarez un champ xde type Animal.
  • Passez xen outparamètre à N.
  • Nécrit un Tigerdans n, qui est un alias pour x.
  • Sur un autre fil, quelqu'un écrit un Turtledansx .
  • Ntente de lire le contenu de n, et découvre un Turtledans ce qu'il pense être une variable de type Mammal.

De toute évidence, nous voulons rendre cela illégal.

Conclusion 4 : les outparamètres ne peuvent pas être rendus "plus grands".


Conclusion finale : ni refniout les paramètres peuvent varier leurs types. Faire autrement, c'est briser la sécurité de type vérifiable.

Si ces problèmes de théorie de base des types vous intéressent, envisagez de lire ma série sur le fonctionnement de la covariance et de la contravariance en C # 4.0 .

Eric Lippert
la source
6
+1. Une bonne explication utilisant des exemples de classe du monde réel qui démontrent clairement les problèmes (c'est-à-dire - expliquer avec A, B et C rend plus difficile de démontrer pourquoi cela ne fonctionne pas).
Grant Wagner
4
Je suis ému de lire ce processus de réflexion. Je pense que je ferais mieux de revenir aux livres!
Scott McKenzie
Dans ce cas, nous ne pouvons vraiment pas utiliser la variable de classe abstraite comme arguments et transmettre son objet de classe dérivé !!
Prashant Cholachagudda
Pourtant, pourquoi les outparamètres ne peuvent-ils pas être «plus grands»? La séquence que vous avez décrite peut être appliquée à n'importe quelle variable, pas seulement à une variable de outparamètre. Et aussi le lecteur doit convertir la valeur de l'argument Mammalavant de tenter d'y accéder car Mammalet bien sûr, il peut échouer s'il n'est pas attentionné
astef
29

Parce que dans les deux cas, vous devez pouvoir attribuer une valeur au paramètre ref / out.

Si vous essayez de passer b dans la méthode Foo2 comme référence, et dans Foo2 vous essayez d'assigner a = new A (), ce serait invalide.
Même raison pour laquelle vous ne pouvez pas écrire:

B b = new A();
maciejkow
la source
+1 Droit au but et explique parfaitement bien la raison.
Rui Craveiro
10

Vous êtes aux prises avec le problème classique de la POO de la covariance (et de la contravariance), voir wikipedia : bien que ce fait puisse défier les attentes intuitives, il est mathématiquement impossible d'autoriser la substitution des classes dérivées au lieu des classes de base pour des arguments mutables (assignables) (et également des conteneurs dont les articles sont assignables, pour la même raison) tout en respectant le principe de Liskov . Pourquoi il en est ainsi est esquissé dans les réponses existantes, et exploré plus en profondeur dans ces articles wiki et les liens qui en découlent.

Les langages POO qui semblent le faire tout en restant traditionnellement sécurisés statiquement sont la «triche» (insertion de vérifications de type dynamiques cachées, ou exigeant un examen à la compilation de TOUTES les sources à vérifier); le choix fondamental est: soit abandonner cette covariance et accepter la perplexité des praticiens (comme le fait C # ici), soit passer à une approche de typage dynamique (comme l'a fait le tout premier langage POO, Smalltalk), ou passer à immuable (simple- affectation), comme le font les langages fonctionnels (sous immuabilité, vous pouvez prendre en charge la covariance, et également éviter d'autres énigmes connexes telles que le fait que vous ne pouvez pas avoir de sous-classe Square Rectangle dans un monde de données mutables).

Alex Martelli
la source
4

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

Henk Holterman
la source
C'est plus le type inféré de "b" non clair en raison de la var qui est le problème.
Je suppose qu'à la ligne 6 vous vouliez dire => B b = null;
Alejandro Miralles
@amiralles - oui, varc'était totalement faux. Fixé.
Henk Holterman le
4

Alors que les autres réponses ont brièvement expliqué le raisonnement derrière ce comportement, je pense qu'il vaut la peine de mentionner que si vous avez vraiment besoin de faire quelque chose de cette nature, vous pouvez accomplir des fonctionnalités similaires en faisant de Foo2 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 {}  
}
BrendanLoBuglio
la source
2

Parce que donner Foo2un ref Baurait pour résultat un objet mal formé parce Foo2que ne sait comment remplir Aqu'une partie de B.

CannibaleSmith
la source
0

N'est-ce pas le compilateur qui vous dit qu'il aimerait que vous lanciez explicitement l'objet afin qu'il puisse être sûr que vous savez quelles sont vos intentions?

Foo2(ref (A)b)
dlamblin
la source
Impossible de faire ça, "Un argument ref ou out doit être une variable assignable"
0

Cela a du sens du point de vue de la sécurité, mais j'aurais préféré que le compilateur donne un avertissement au lieu d'une erreur, car il existe des utilisations légitimes d'objets polymoprhiques 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 cela fonctionnerait-il?

Oofpez
la source
0

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 l' ancêtre ( ie Object ):

void Foo2(ref Object connection) { }

Qu'est-ce qui ne va pas avec ça?

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

Vous venez de réussir à attribuer un Bitmapà votre fichier SqlConnection.

Ce n'est pas bon.


Réessayez avec d'autres:

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

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

Vous avez bourré un OracleConnectionsur-dessus de votre SqlConnection.

Ian Boyd
la source
0

Dans mon cas, ma fonction a accepté un objet et je ne pouvais rien envoyer alors j'ai simplement fait

object bla = myVar;
Foo(ref bla);

Et ça marche

Mon Foo est dans VB.NET et il vérifie le type à l'intérieur et fait beaucoup de logique

Je m'excuse si ma réponse est en double mais les autres étaient trop longues

Shereef Marzouk
la source