Perché questo codice polimorfico C # stampa ciò che fa?

Di recente mi è stato dato il seguente pezzo di codice come una specie di puzzle per aiutare a comprendere il Polymorphism e l’ Inheritance in OOP – C #.

 // No compiling! public class A { public virtual string GetName() { return "A"; } } public class B:A { public override string GetName() { return "B"; } } public class C:B { public new string GetName() { return "C"; } } void Main() { A instance = new C(); Console.WriteLine(instance.GetName()); } // No compiling! 

Ora, dopo una lunga e lunga chiacchierata con l’altro sviluppatore che ha presentato l’enigma, so qual è l’output, ma non voglio rovinarlo per te. L’unico problema che sto affrontando è il modo in cui arriviamo a quell’output, come il codice passa, cosa eredita cosa, ecc.

Pensavo che C sarebbe tornato in quanto sembra essere la class che è definita. Poi ho riflettuto sul fatto che B sarebbe stato restituito perché C eredita B – ma B eredita anche A (che è il punto in cui mi sono confuso!).


Domanda:

Qualcuno potrebbe spiegare come il polimorfismo e l’ereditarietà giochino il loro ruolo nel recuperare l’output, eventualmente visualizzato sullo schermo?

Il modo corretto di pensarci è immaginare che ogni class richieda che i suoi oggetti abbiano un certo numero di “slot”; quelle slot sono piene di metodi. La domanda “quale metodo viene effettivamente chiamato?” richiede di capire due cose:

  1. Quali sono i contenuti di ogni slot?
  2. Quale slot è chiamato?

Iniziamo considerando le slot. Ci sono due slot. Tutte le istanze di A devono disporre di uno slot che chiameremo GetNameSlotA. Tutte le istanze di C devono disporre di uno slot che chiameremo GetNameSlotC. Questo è ciò che significa “nuovo” nella dichiarazione in C – significa “Voglio un nuovo spazio”. Rispetto al “override” della dichiarazione in B, che significa “Non voglio un nuovo slot, voglio riutilizzare GetNameSlotA”.

Certamente, C eredita da A, quindi C deve anche avere uno slot GetNameSlotA. Pertanto, le istanze di C hanno due slot: GetNameSlotA e GetNameSlotC. Le istanze di A o B che non hanno C hanno uno slot, GetNameSlotA.

Ora, cosa entra in questi due slot quando crei una nuova C? Esistono tre metodi, che chiameremo GetNameA, GetNameB e GetNameC.

La dichiarazione di A dice “metti GetNameA in GetNameSlotA”. A è una superclass di C, quindi la regola di A si applica a C.

La dichiarazione di B dice “metti GetNameB in GetNameSlotA”. B è una superclass di C, quindi la regola di B si applica alle istanze di C. Ora abbiamo un conflitto tra A e B. B è il tipo più derivato, quindi vince: la regola di B sovrascrive la regola di A. Da qui la parola “override” nella dichiarazione.

La dichiarazione di C dice “metti GetNameC in GetNameSlotC”.

Pertanto, la tua nuova C avrà due slot. GetNameSlotA conterrà GetNameB e GetNameSlotC conterrà GetNameC.

Ora abbiamo determinato quali sono i metodi in quali slot, quindi abbiamo risposto alla nostra prima domanda.

Ora dobbiamo rispondere alla seconda domanda. Quale slot è chiamato?

Pensaci come se fossi il compilatore. Hai una variabile. Tutto quello che sai è che è di tipo A. Ti viene chiesto di risolvere una chiamata di metodo su quella variabile. Guardate gli slot disponibili su un A, e l’unico slot che potete trovare corrisponde a GetNameSlotA. Non sai di GetNameSlotC, perché hai solo una variabile di tipo A; perché dovresti cercare slot che si applicano solo a C?

Quindi questa è una chiamata a qualunque cosa si trovi in ​​GetNameSlotA. Abbiamo già stabilito che in fase di esecuzione, GetNameB sarà in quello slot. Pertanto, questa è una chiamata a GetNameB.

La chiave da asporto qui è che in C # la risoluzione di sovraccarico sceglie uno slot e genera una chiamata a qualsiasi cosa accada nello slot.

Dovrebbe restituire “B” perché B.GetName() è contenuto nella piccola casella della tabella virtuale per la funzione A.GetName() . C.GetName() è un “override” della compilazione, non sovrascrive la tabella virtuale, quindi non è ansible recuperarla tramite un puntatore ad A

Semplice, devi solo tenere a mente l’albero dell’eredità.

Nel tuo codice, tieni un riferimento a una class di tipo ‘A’, che viene istanziata da un’istanza di tipo ‘C’. Ora, per risolvere l’esatto indirizzo del metodo per il metodo virtuale ‘GetName ()’, il compilatore sale la gerarchia dell’ereditarietà e cerca l’ override più recente (si noti che solo ‘virtuale’ è un override, ‘nuovo’ è qualcosa di completamente diverso …).

Questo è in breve cosa succede. La nuova parola chiave dal tipo “C” giocherebbe un ruolo solo se la chiamassi su un’istanza di tipo “C” e il compilatore negherebbe del tutto tutte le possibili relazioni di ereditarietà. Rigorosamente parlato, questo non ha nulla a che fare con il polimorfismo – lo si può vedere dal fatto che se si maschera un metodo virtuale o non virtuale con la parola chiave ‘nuova’ non fa alcuna differenza …

“Nuovo” nella class “C” significa esattamente questo: se chiami “GetName ()” su un’istanza di questo tipo (esatto), dimentica tutto e utilizza questo metodo. ‘Virtuale’ in senso contrario significa: Vai su l’albero di ereditarietà finché non trovi un metodo con questo nome, indipendentemente dal tipo esatto dell’istanza chiamante.

OK, il post è un po ‘vecchio, ma è una domanda eccellente e una risposta eccellente, quindi volevo solo aggiungere i miei pensieri.

Considera il seguente esempio, che è lo stesso di prima, ad eccezione della funzione principale:

 // No compiling! public class A { public virtual string GetName() { return "A"; } } public class B:A { public override string GetName() { return "B"; } } public class C:B { public new string GetName() { return "C"; } } void Main() { Console.Write ( "Type a or c: " ); string input = Console.ReadLine(); A instance = null; if ( input == "a" ) instance = new A(); else if ( input == "c" ) instance = new C(); Console.WriteLine( instance.GetName() ); } // No compiling! 

Ora è davvero ovvio che la chiamata alla funzione non può essere associata a una funzione specifica in fase di compilazione. Tuttavia, qualcosa deve essere compilato e tale informazione può dipendere solo dal tipo di riferimento. Quindi, sarebbe imansible eseguire la funzione GetName della class C con qualsiasi riferimento diverso da uno di tipo C.

PS Forse avrei dovuto usare il termine metodo anziché la funzione, ma come disse Shakespeare: Una funzione con qualsiasi altro nome è ancora una funzione 🙂

In realtà, penso che dovrebbe visualizzare C, perché il nuovo operatore nasconde semplicemente tutti i metodi degli antenati con lo stesso nome. Quindi, con i metodi di A e B nascosti, rimane visibile solo C.

http://msdn.microsoft.com/en-us/library/51y09td4%28VS.71%29.aspx#vclrfnew_newmodifier