Ich glaube, jeder ist mit den drei Merkmalen der objektorientierten Programmierung vertraut: Kapselung, Vererbung und Polymorphismus. Jeder kann ein paar Worte dazu sagen, aber die meisten wissen nur, was sie sind, nicht wie die CLR sie intern implementiert. Daher diskutiert dieser Artikel hauptsächlich einige Konzepte des Polymorphismus und den internen Implementierungsmechanismus.

1. Konzept des Polymorphismus

Zuerst erklären wir, was Polymorphismus ist: Dieselbe Operation, auf verschiedene Objekte angewendet, kann unterschiedliche Interpretationen haben und unterschiedliche Ausführungsergebnisse liefern. Das ist Polymorphismus. Anders ausgedrückt: Eine Instanz desselben Typs ruft die "gleiche" Methode auf, aber die Ergebnisse sind unterschiedlich. Das "gleiche" steht hier in Anführungszeichen, weil die Methoden, die gleich aussehen, tatsächlich unterschiedliche Methoden sind, die aufgerufen werden.

Wenn es um Polymorphismus geht, kommen wir nicht umhin, die folgenden Konzepte zu erwähnen: Überladen, Überschreiben, virtuelle Methoden, abstrakte Methoden und verdeckende Methoden. Stellen wir ihre Konzepte einzeln vor.

1, ÜberladenIm selben Gültigkeitsbereich (im Allgemeinen eine Klasse) werden zwei oder mehr Methoden mit demselben Methodennamen, aber unterschiedlichen Parameterlisten als Überladen bezeichnet. Sie haben drei Merkmale (allgemein bekannt als "zwei Muss und ein Kann"):

  • Methodennamen müssen gleich sein
  • Parameterlisten müssen unterschiedlich sein
  • Rückgabetypen können unterschiedlich sein

Zum Beispiel:

public void Sleep()
{
    Console.WriteLine("Animal睡觉");
}
public int Sleep(int time)
{
    Console.WriteLine("Animal{0}点睡觉", time);
    return time;
}

2, ÜberschreibenIn einer Unterklasse erfordert das Neudefinieren einer anderen Implementierung einer Methode, um ihre eigenen Bedürfnisse zu erfüllen, das override-Schlüsselwort. Die überschriebene Methode muss eine virtuelle Methode sein, die das virtual-Schlüsselwort verwendet. Ihre Merkmale sind (drei gleich):

  • Gleicher Methodenname
  • Gleiche Parameterliste
  • Gleicher Rückgabetyp

Zum Beispiel: Definition in der Elternklasse:

public virtual void EatFood()
{
    Console.WriteLine("Animal吃东西");
}

Definition in der abgeleiteten Klasse:

public override void EatFood()
{
    Console.WriteLine("Cat吃东西");
    //base.EatFood();
}

Tipp:Häufig fragen Freunde nach dem Unterschied zwischen Überladen und Überschreiben, und im Internet ist dies eine der üblichen C#-Interviewfragen. Tatsächlich haben diese beiden Konzepte überhaupt keine Beziehung zueinander; sie enthalten lediglich beide die Zeichenfolge "over". Es ist nicht sinnvoll, sie zu vergleichen. Unterscheiden Sie einfach ihre unterschiedlichen Definitionen.

3, Virtuelle MethodeEine in der Basisklasse definierte Methode, die das Überschreiben in abgeleiteten Klassen ermöglicht und mit dem Schlüsselwort virtual definiert wird. Zum Beispiel:

public virtual void EatFood()
{
    Console.WriteLine("Animal吃东西");
}

HinweisVirtuelle Methoden können auch direkt aufgerufen werden. Zum Beispiel:

Animal a = new Animal();
a.EatFood();

Die Ausgabe ist:

Animal吃东西

4, Abstrakte MethodeEine in der Basisklasse definierte Methode, die in abgeleiteten Klassen überschrieben werden muss. Sie wird mit dem Schlüsselwortabstractdefiniert. Zum Beispiel:

public abstract class Biology
{
    public abstract void Live();
}
public class Animal : Biology
{
    public override void Live()
    {
        Console.WriteLine("Animal重写的抽象方法");
        //throw new NotImplementedException();
    } 
}

Hinweis:Abstrakte Methoden können nur in abstrakten Klassen definiert werden. Wenn sie nicht in einer abstrakten Klasse definiert werden, wird der folgende Fehler gemeldet:

Der Unterschied zwischen virtuellen Methoden und abstrakten Methoden ist:Da abstrakte Klassen nicht instanziiert werden können, können abstrakte Methoden nicht aufgerufen werden; mit anderen Worten, abstrakte Methoden können niemals implementiert werden.

5, Ausblendende MethodeEine in einer abgeleiteten Klasse definierte Methode mit demselben Namen wie eine Methode in der Basisklasse, definiert mit dem Schlüsselwort new. Zum Beispiel gibt es in der Basisklasse Animal eine Methode Sleep():

public void Sleep()
{
    Console.WriteLine("Animal Sleep");
}

Dann lautet der Code, um die ausblendende Methode in der abgeleiteten Klasse Cat zu definieren:

new public void Sleep()
{
    Console.WriteLine("Cat Sleep");
}

Oder:

public new void Sleep()
{
    Console.WriteLine("Cat Sleep");
} 

Hinweis:

  • (1) Eine ausblendende Methode kann nicht nur virtuelle Methoden in der Basisklasse ausblenden, sondern auch nicht virtuelle Methoden in der Basisklasse.
  • (2) Bei einer ausblendenden Methode ruft eine Instanz der Elternklasse die Methode der Elternklasse auf, und eine Instanz der Kindklasse ruft die Methode der Kindklasse auf.
  • (3) Im Vergleich zum vorherigen Punkt: Bei einer überschreibenden Methode ruft eine Variable der Kindklasse die überschriebene Methode der Kindklasse auf. Bei einer Variable der Elternklasse hängt es davon ab, ob die Referenz der Elternklasse auf eine Instanz der Kindklasse oder auf ihre eigene Instanz zeigt. Wenn sie auf eine Instanz der Elternklasse verweist, wird die Methode der Basisklasse aufgerufen; wenn sie auf eine Instanz der abgeleiteten Klasse verweist, wird die Methode der abgeleiteten Klasse aufgerufen.

Okay, die Grundkonzepte sind abgeschlossen. Sehen wir uns ein Beispiel an. Zuerst erstellen wir mehrere Klassen:

public abstract class Biology
{
    public abstract void Live();
}
public class Animal : Biology
{
    public override void Live()
    {
        Console.WriteLine("Animal重写的Live");
        //throw new NotImplementedException();
    }
    public void Sleep()
    {
        Console.WriteLine("Animal Sleep");
    }
    public int Sleep(int time)
    {
        Console.WriteLine("Animal在{0}点Sleep", time);
        return time;
    }
    public virtual void EatFood()
    {
        Console.WriteLine("Animal EatFood");
    }
}
public class Cat : Animal
{
    public override void EatFood()
    {
        Console.WriteLine("Cat EatFood");
        //base.EatFood();
    }
    new public void Sleep()
    {
        Console.WriteLine("Cat Sleep");
    }
    //public new void Sleep()
    //{
    //    Console.WriteLine("Cat Sleep");
    //}
}
public class Dog : Animal
{
    public override void EatFood()
    {
        Console.WriteLine("Dog EatFood");
        //base.EatFood();
    }
}

Sehen wir uns nun den auszuführenden Code an:

class Program
{
    static void Main(string[] args)
    {
        //Animal的实例
        Animal a = new Animal();
        //Animal的实例,引用派生类Cat对象
        Animal ac = new Cat();
        //Animal的实例,引用派生类Dog对象
        Animal ad = new Dog();
        //Cat的实例
        Cat c = new Cat();
        //Dog的实例
        Dog d = new Dog();
        //重载
        a.Sleep();
        a.Sleep(23);
        //重写和虚方法
        a.EatFood();
        ac.EatFood();
        ad.EatFood();
        //抽象方法
        a.Live();
        //隐藏方法
        a.Sleep();
        ac.Sleep();
        c.Sleep();
        Console.ReadKey();
    }
}

Zuerst definieren wir mehrere Instanzen der Klassen, die wir verwenden müssen. Zu beachten ist:

  • (1) Die Klasse Biology ist eine abstrakte Klasse und kann nicht instanziiert werden;
  • (2) Die Variable ac ist eine Instanz von Animal, aber sie zeigt auf ein Cat-Objekt. Da Cat eine abgeleitete Klasse von Animal ist, ist diese Konvertierung in Ordnung. Dies ist auch der Kernpunkt des Polymorphismus.

Als Nächstes analysieren wir Schritt für Schritt:

1,

//重载
a.Sleep();
a.Sleep(23);

Offensichtlich sind die beiden von der Animal-Variable a aufgerufenen Sleep-Methoden überladene Methoden. Die erste Anweisung ruft die parameterlose Methode Sleep() auf, und die zweite Anweisung ruft die Methode Sleep mit einem int-Parameter auf. Beachten Sie, dass die Rückgabetypen der beiden Sleep-Methoden unterschiedlich sind, was auch die letzte der drei Eigenschaften des Überschreibens veranschaulicht – der Rückgabetyp kann unterschiedlich sein.

Das Ausführungsergebnis ist wie folgt:

Animal Sleep
Animal在23点Sleep

2,

//重写和虚方法
a.EatFood();
ac.EatFood();
ad.EatFood();

In diesem Abschnitt sind a, ac und ad alle Instanzen von Animal, aber sie referenzieren unterschiedliche Objekte. a referenziert ein Animal-Objekt, ac referenziert ein Cat-Objekt und ad referenziert ein Dog-Objekt. Welchen Unterschied wird dies in den Ausführungsergebnissen machen? Sehen Sie sich die Ausführungsergebnisse unten an:

Animal EatFood
Cat EatFood
Dog EatFood

Die erste Anweisung: Eine Animal-Instanz ruft direkt die virtuelle Methode EatFood von Animal auf, kein Problem.

In der zweiten und dritten Anweisung sind sie zwar ebenfalls Instanzen von Animal, verweisen aber auf Cat- bzw. Dog-Objekte, sodass die in den Klassen Cat und Dog überschriebenen EatFood-Methoden aufgerufen werden, genauso als ob Cat- und Dog-Instanzen direkt EatFood aufgerufen hätten. Dies ist die Verkörperung von Polymorphie: Dieselbe Operation, angewendet auf verschiedene Objekte, kann unterschiedlich interpretiert werden und unterschiedliche Ausführungsergebnisse liefern.

3,

//抽象方法
a.Live();

Das ist relativ einfach. Es überschreibt direkt die Live-Methode der Elternklasse Biology. Das Ausführungsergebnis ist wie folgt:

Animal重写的Live

4,

//隐藏方法
a.Sleep();
ac.Sleep();
c.Sleep();

Bei der Analyse der verdeckenden Methode sollte sie mit virtuellen Methoden und Überschreiben verglichen werden. Es gibt keinen Einwand dagegen, dass Variable a die Sleep-Methode von Animal aufruft und Variable c die Sleep-Methode von Cat aufruft. Aber Variable ac referenziert ein Objekt vom Typ Cat. Soll sie die EatFood-Methode vom Typ Animal oder die EatFood-Methode vom Typ Cat aufrufen? Die Antwort ist, dass sie die Elternklasse aufruft, nämlich die EatFood-Methode von Animal. Das Ausführungsergebnis ist wie folgt:

Animal Sleep
Animal Sleep
Cat Sleep

Die meisten Artikel enden hier, lassen uns lediglich diese Konzepte und das Aufrufen von Methoden kennen, ohne zu erklären, warum das so ist. Als Nächstes gehen wir etwas tiefer und sprechen über den Mechanismus hinter der Polymorphie.


2. Vertieftes Verständnis von Polymorphie

Um Polymorphie tiefgehend zu verstehen, müssen wir mit Werttypen und Referenztypen beginnen. Wir alle wissen, dass Werttypen auf dem Thread-Stack gespeichert werden, während Referenztypen im verwalteten Heap gespeichert werden. Da alle Klassen Referenztypen sind, betrachten wir nur Referenztypen.

Kehren wir nun zum früheren Beispiel zurück. Main ist der Programmeinstiegspunkt. Wenn der JIT-Compiler die Main-Funktion in native CPU-Anweisungen kompiliert, stellt er fest, dass diese Methode auf die Klassen Biology, Animal, Cat und Dog verweist, sodass die CLR mehrere Instanzen erstellt, um diese Typen selbst darzustellen. Wir nennen sie "Typobjekte". Dieses Objekt enthält die statischen Felder der Klasse, eine Methodentabelle mit allen Methoden der Klasse und außerdem zwei zusätzliche Member, die alle Objekte im verwalteten Heap haben müssen – den Typobjektzeiger und den Synchronisierungsblockindex.

Der obige Abschnitt ist möglicherweise für manche Leser, die keine relevanten CLR-Bücher gelesen haben, nicht verständlich, daher lassen Sie uns ein Diagramm zeichnen, um ihn zu beschreiben:

Das obige Diagramm zeigt, was die CLR vor der Ausführung der Main-Funktion tut. Als Nächstes beginnen wir mit der Ausführung der Main-Funktion (der Einfachheit halber die Main-Funktion vereinfachen):

//Animal的实例
Animal a = new Animal();
//Animal的实例,引用派生类Cat对象
Animal ac = new Cat();
//Animal的实例,引用派生类Dog对象
Animal ad = new Dog();
a.Sleep();
a.EatFood();
ac.EatFood();
ad.EatFood();

Nun werden drei Animal-Instanzen instanziiert, aber sie verweisen tatsächlich auf ein Animal-Objekt, ein Cat-Objekt bzw. ein Dog-Objekt, wie unten gezeigt:

Bitte beachten Sie, dass die Variablen ac und ad zwar beide vom Typ Animal sind, aber auf ein Cat-Objekt bzw. ein Dog-Objekt verweisen. Das ist der entscheidende Punkt.

Bei der Ausführung von a.Sleep() wird der JIT-Compiler, da Sleep eine nicht-virtuelle Instanzmethode ist, das Typobjekt (Animal-Typobjekt) finden, das dem Typ (Animal) der Variable (a) entspricht, die den Aufruf gemacht hat. Dann ruft er die Sleep-Methode in diesem Typobjekt auf. Wenn dieses Typobjekt keine Sleep-Methode hat, durchläuft der JIT-Compiler rückwärts die Basisklassen der Klasse (bis hinauf zu Object), um die Sleep-Methode zu finden.

Wenn ac.EatFood ausgeführt wird, generiert der JIT-Compiler beim Aufruf zusätzlichen Code in der Methode, weil EatFood eine virtuelle Instanzmethode ist. Dieser Code prüft zuerst die Variable (ac), die den Aufruf ausgeführt hat, folgt dann der Referenzadresse der Variable, um das Objekt zu finden, das den Aufruf ausgeführt hat (Cat-Objekt), findet das Typobjekt, das dem Objekt entspricht, das den Aufruf ausgeführt hat (Cat-Typobjekt), und sucht schließlich in diesem Typobjekt nach der EatFood-Methode. Ebenso: Wenn die EatFood-Methode in diesem Typobjekt nicht gefunden wird, geht der JIT-Compiler bis zur Basisklasse dieses Typobjekts zurück, um sie zu finden.

Das oben Beschriebene sind die unterschiedlichen Ausführungsweisen des JIT-Compilers, wenn er auf Aufrufe nicht virtueller Instanzmethoden und virtueller Instanzmethoden trifft. Es ist diese unterschiedliche Behandlung dieser beiden Methodenarten, die eines der drei Merkmale der objektorientierten Programmierung hervorbringt, das wir an der Oberfläche sehen – Polymorphie.

Original-URL: https://www.cnblogs.com/zhangkai2237/archive/2012/12/20/2826734.html