Perché 'ref' e 'out' non supportano il polimorfismo?


124

Prendi il seguente:

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) {}  
}

Perché si verifica l'errore di compilazione sopra riportato? Questo succede con entrambi refe outargomenti.

Risposte:


169

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

AGGIORNAMENTO: ho usato questa risposta come base per questo post di blog:

Perché i parametri ref e out non consentono la variazione del tipo?

Vedi la pagina del blog per ulteriori commenti su questo problema. Grazie per l'ottima domanda.

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

Supponiamo di avere classi Animal, Mammal, Reptile, Giraffe, Turtlee Tiger, con le relazioni sottoclasse evidenti.

Supponiamo ora di avere un metodo void M(ref Mammal m). Mpuò sia leggere che scrivere m.


Puoi passare una variabile di tipo Animala M?

No. Quella variabile potrebbe contenere un Turtle, ma Msupporrà che contenga solo Mammiferi. A Turtlenon è un Mammal.

Conclusione 1 : i refparametri non possono essere "ingranditi". (Ci sono più animali che mammiferi, quindi la variabile sta diventando "più grande" perché può contenere più cose.)


Puoi passare una variabile di tipo Giraffea M?

No. Mpuò scrivere me Mpotrebbe voler scrivere a Tigerin m. Ora hai inserito a Tigerin una variabile che è in realtà di tipo Giraffe.

Conclusione 2 : i refparametri non possono essere "più piccoli".


Adesso considera N(out Mammal n).

Puoi passare una variabile di tipo Giraffea N?

No. Npuò scrivere ne Npotrebbe voler scrivere a Tiger.

Conclusione 3 : i outparametri non possono essere "più piccoli".


Puoi passare una variabile di tipo Animala N?

Hmm.

Beh perchè no? Nnon riesco a leggere n, può solo scrivergli, giusto? Scrivi Tigera una variabile di tipo Animale sei pronto, giusto?

Sbagliato. La regola non è " Npuò solo scrivere n".

Le regole sono, brevemente:

1) Ndeve scrivere nprima di Nrestituire normalmente. (Se Nviene lanciato, tutte le scommesse sono disattivate.)

2) Ndeve scrivere qualcosa nprima di leggere qualcosa da n.

Ciò consente questa sequenza di eventi:

  • Dichiarare un campo xdi tipo Animal.
  • Passa xcome outparametro a N.
  • Nscrive a Tigerin n, che è un alias per x.
  • Su un altro thread, qualcuno scrive a Turtlein x.
  • Ntenta di leggere il contenuto ne scopre a Turtlein ciò che pensa sia una variabile di tipo Mammal.

Chiaramente vogliamo renderlo illegale.

Conclusione 4 : i outparametri non possono essere "ingranditi".


Conclusione finale : refoutparametri possono variare i loro tipi. Fare altrimenti significa rompere la sicurezza verificabile del tipo.

Se questi problemi nella teoria dei tipi di base ti interessano, potresti leggere le mie serie su come funzionano la covarianza e la contravarianza in C # 4.0 .


6
+1. Grande spiegazione usando esempi di classe del mondo reale che dimostrano chiaramente i problemi (es. Spiegare con A, B e C rende più difficile dimostrare perché non funziona).
Grant Wagner,

4
Mi sento umiliato leggendo questo processo di pensiero. Penso che farò meglio a tornare ai libri!
Scott McKenzie,

In questo caso, non possiamo davvero usare la variabile di classe Abstract come argomenti e passare l'oggetto di classe derivata !!
Prashant Cholachagudda,

Tuttavia, perché i outparametri non possono essere "ingranditi"? La sequenza che hai descritto può essere applicata a qualsiasi variabile, non solo alla outvariabile parametro. E anche il lettore deve esprimere il valore dell'argomento Mammalprima di tentare di accedervi poiché, Mammalnaturalmente, può fallire se non è
attento

29

Perché in entrambi i casi devi essere in grado di assegnare valore al parametro ref / out.

Se si tenta di passare b nel metodo Foo2 come riferimento e in Foo2 si tenta di valutare a = new A (), ciò non sarebbe valido.
Stesso motivo per cui non puoi scrivere:

B b = new A();

+1 Dritto al punto e spiega perfettamente il motivo.
Rui Craveiro,

10

Stai lottando con il classico problema OOP della covarianza (e della contraddizione), vedi Wikipedia : per quanto questo fatto possa sfidare le aspettative intuitive, è matematicamente impossibile consentire la sostituzione di classi derivate al posto di quelle di base per argomenti mutabili (assegnabili) (e anche contenitori i cui oggetti sono assegnabili, per lo stesso motivo) pur rispettando il principio di Liskov . Perché è così è abbozzato nelle risposte esistenti ed esplorato più in profondità in questi articoli wiki e collegamenti da essi.

I linguaggi OOP che sembrano farlo mentre rimangono tradizionalmente staticamente dattiloscritti stanno "barando" (inserendo controlli di tipo dinamici nascosti o richiedendo un esame in fase di compilazione di TUTTE le fonti per controllare); la scelta fondamentale è: o rinunciare a questa covarianza e accettare la perplessità dei praticanti (come fa C # qui), oppure passare a un approccio di tipizzazione dinamica (come ha fatto il primo linguaggio OOP, Smalltalk,) o passare a immutabile (single- assegnazione), come fanno i linguaggi funzionali (sotto l'immutabilità, puoi supportare la covarianza ed evitare anche altri enigmi correlati come il fatto che non puoi avere una sottoclasse quadrata Rectangle in un mondo di dati mutabili).


4

Tener conto di:

class C : A {}
class B : A {}

void Foo2(ref A a) { a = new C(); } 

B b = null;
Foo2(ref b);

Violerebbe la sicurezza del tipo


È più il tipo dedotto poco chiaro di "b" a causa della var che è il problema lì.

Immagino che alla riga 6 intendevi => B b = null;
Alejandro Miralles,

@amiralles - sì, varera totalmente sbagliato. Fisso.
Henk Holterman,

4

Mentre le altre risposte hanno brevemente spiegato il ragionamento alla base di questo comportamento, penso che valga la pena menzionare che se hai davvero bisogno di fare qualcosa del genere puoi ottenere funzionalità simili trasformando Foo2 in un metodo generico, come tale:

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 {}  
}

2

Perché dare Foo2un ref Bcomporterebbe un oggetto malformato perché Foo2sa solo come riempire Aparte di B.


0

Il compilatore non ti sta dicendo che ti piacerebbe lanciare esplicitamente l'oggetto in modo che tu possa essere sicuro di sapere quali sono le tue intenzioni?

Foo2(ref (A)b)

Non posso farlo, "Un argomento ref o out deve essere una variabile assegnabile"

0

Ha un senso dal punto di vista della sicurezza, ma l'avrei preferito se il compilatore avesse dato un avvertimento invece di un errore, dal momento che ci sono usi legittimi di oggetti polimoprimici passati per riferimento. per esempio

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);
}

Questo non verrà compilato, ma funzionerebbe?


0

Se usi esempi pratici per i tuoi tipi, lo vedrai:

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

E ora hai la tua funzione che prende l' antenato ( cioè Object ):

void Foo2(ref Object connection) { }

Cosa può esserci di sbagliato in questo?

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

Sei appena riuscito a assegnare un Bitmapal tuo SqlConnection.

Non va bene.


Riprova con altri:

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

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

Hai riempito una OracleConnectionparte superiore del tuo SqlConnection.


0

Nel mio caso la mia funzione ha accettato un oggetto e non ho potuto inviare nulla, così ho semplicemente fatto

object bla = myVar;
Foo(ref bla);

E questo funziona

Il mio Foo è in VB.NET e controlla il tipo all'interno e fa molta logica

Mi scuso se la mia risposta è duplicata, ma altre erano troppo lunghe

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.