Perché 'ref' e 'out' non supportano il polimorfismo?
-
05-07-2019 - |
Domanda
Prendi quanto segue:
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? Ciò accade con entrambi gli argomenti ref
e out
.
Soluzione
=============
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 che tu abbia classi Animal
, Mammal
, Reptile
, Giraffe
, Turtle
e Tiger
, con le evidenti relazioni di sottoclasse.
Ora supponiamo di avere un metodo void M (ref Mammal m)
. M
può sia leggere che scrivere m
.
Puoi passare una variabile di tipo
Animal
aM
?
No. Tale variabile potrebbe contenere un Turtle
, ma M
supporrà che contenga solo mammiferi. Un Turtle
non è un Mammal
.
Conclusione 1 : i parametri ref
non possono essere resi " 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
Giraffe
aM
?
No. M
può scrivere su m
e M
potrebbe voler scrivere un Tiger
in m
. Ora hai inserito un Tiger
in una variabile che è in realtà del tipo Giraffe
.
Conclusione 2 : i parametri ref
non possono essere fatti " più piccoli " ;.
Ora considera N (fuori Mammal n)
.
Puoi passare una variabile di tipo
Giraffe
aN
?
No. N
può scrivere su n
e N
potrebbe voler scrivere un Tiger
.
Conclusione 3 : i parametri out
non possono essere resi " più piccoli " ;.
Puoi passare una variabile di tipo
Animal
aN
?
Hmm.
Bene, perché no? N
non può leggere da n
, può solo scrivergli, giusto? Scrivi un Tiger
su una variabile di tipo Animal
e sei pronto, giusto?
sbagliato. La regola non è " N
può scrivere solo su n
" ;.
Le regole sono, brevemente:
1) N
deve scrivere su n
prima che N
ritorni normalmente. (Se N
viene lanciato, tutte le scommesse sono disattivate.)
2) N
deve scrivere qualcosa su n
prima di leggere qualcosa da n
.
Ciò consente questa sequenza di eventi:
- Dichiara un campo
x
di tipoAnimal
. - Passa
x
come parametroout
aN
. -
N
scrive unTiger
inn
, che è un alias perx
. - Su un altro thread, qualcuno scrive un
Turtle
inx
. -
N
tenta di leggere il contenuto din
e scopre unTurtle
in ciò che pensa sia una variabile di tipoMammal
.
Chiaramente vogliamo renderlo illegale.
Conclusione 4 : i parametri out
non possono essere impostati " ingranditi "
Conclusione finale : Né i parametri ref
né out
possono variare nei loro tipi. Fare altrimenti significa rompere la sicurezza verificabile del tipo.
Se questi problemi nella teoria dei tipi di base ti interessano, potresti leggere la mia serie su come funzionano la covarianza e la contraddizione in C # 4.0 .
Altri suggerimenti
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();
Stai lottando con il classico problema OOP di covarianza (e 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 elementi sono assegnabili, per lo stesso motivo) pur rispettando Principio di Liskov . Perché è così è abbozzato nelle risposte esistenti ed esplorato più approfonditamente in questi articoli wiki e collegamenti da essi.
Le lingue OOP che sembrano farlo mentre restano tradizionalmente tipicamente sicure sono "barare". (inserendo controlli di tipo dinamici nascosti o richiedendo l'esame in fase di compilazione di TUTTE le fonti da verificare); 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).
Si consideri:
class C : A {}
class B : A {}
void Foo2(ref A a) { a = new C(); }
B b = null;
Foo2(ref b);
Violerebbe la sicurezza dei tipi
Mentre le altre risposte hanno brevemente spiegato il ragionamento alla base di questo comportamento, penso che valga la pena ricordare che se hai davvero bisogno di fare qualcosa di questa natura 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 {}
}
Perché dare a Foo2
un ref B
comporterebbe un oggetto non valido perché Foo2
sa solo come riempire A
parte di B
.
Il compilatore non ti sta dicendo che vorrebbe che tu lanciassi esplicitamente l'oggetto in modo da poter essere sicuro di sapere quali sono le tue intenzioni?
Foo2(ref (A)b)
Ha senso dal punto di vista della sicurezza, ma l'avrei preferito se il compilatore avesse dato un avvertimento anziché un errore, poiché ci sono usi legittimi di oggetti polimoprimici passati per riferimento. per es.
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?
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 il 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 ad assegnare un Bitmap
al 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 un OracleConnection
sopra il tuo SqlConnection
.
Nel mio caso la mia funzione ha accettato un oggetto e non ho potuto inviare nulla, quindi l'ho fatto semplicemente
object bla = myVar;
Foo(ref bla);
E funziona
My 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