Je pense que tout le monde connaît les trois caractéristiques de la programmation orientée objet : l’encapsulation, l’héritage et le polymorphisme. Chacun peut en dire un mot ou deux, mais la plupart ne savent que ce qu’elles sont, pas comment le CLR les implémente en interne. Cet article traite donc principalement de certains concepts du polymorphisme et du mécanisme d’implémentation interne.
1. Concept de polymorphisme
Tout d'abord, expliquons ce qu'est le polymorphisme : la même opération appliquée à des objets différents peut avoir des interprétations différentes et produire des résultats d'exécution différents. C'est cela, le polymorphisme. Autrement dit, une instance du même type appelle la méthode « même », mais les résultats sont différents. Le « même » ici est entre guillemets parce que les méthodes qui semblent identiques sont en réalité des méthodes différentes qui sont appelées.
Quand on parle de polymorphisme, on ne peut éviter de mentionner les concepts suivants : la surcharge, la redéfinition, les méthodes virtuelles, les méthodes abstraites et le masquage de méthodes. Présentons leurs concepts un par un.
1, SurchargeDans une même portée (généralement une classe), deux méthodes ou plus ayant le même nom de méthode mais des listes de paramètres différentes sont appelées une surcharge. Elles ont trois caractéristiques (communément appelées « deux obligatoires et une possibilité ») :
- Les noms de méthodes doivent être identiques
- Les listes de paramètres doivent être différentes
- Les types de retour peuvent être différents
Par exemple :
public void Sleep()
{
Console.WriteLine("Animal睡觉");
}
public int Sleep(int time)
{
Console.WriteLine("Animal{0}点睡觉", time);
return time;
}
2, RedéfinitionDans une sous-classe, pour répondre à ses propres besoins, redéfinir une implémentation différente d'une méthode nécessite le mot-clé override. La méthode redéfinie doit être une méthode virtuelle, à l'aide du mot-clé virtual. Ses caractéristiques sont (trois identiques) :
- Même nom de méthode
- Même liste de paramètres
- Même type de retour
Par exemple : définition dans la classe parente :
public virtual void EatFood()
{
Console.WriteLine("Animal吃东西");
}
Définition dans la classe enfant :
public override void EatFood()
{
Console.WriteLine("Cat吃东西");
//base.EatFood();
}
Astuce :Souvent, certains amis demandent la différence entre la surcharge et la redéfinition, et sur Internet c'est l'une des questions d'entretien C# courantes. En fait, ces deux concepts n'ont aucun rapport ; ils contiennent simplement tous les deux la chaîne "over". Il n'est pas pertinent de les comparer. Distinguez simplement leurs définitions différentes.
3, Méthode virtuelleUne méthode définie dans la classe de base qui permet la redéfinition dans les classes dérivées, définie à l'aide du mot-clé virtual. Par exemple :
public virtual void EatFood()
{
Console.WriteLine("Animal吃东西");
}
RemarqueLes méthodes virtuelles peuvent aussi être appelées directement. Par exemple :
Animal a = new Animal(); a.EatFood();
La sortie est :
Animal吃东西
4, Méthode abstraiteUne méthode définie dans la classe de base et qui doit être redéfinie dans les classes dérivées. Elle est définie à l'aide du mot-cléabstract. Par exemple :
public abstract class Biology
{
public abstract void Live();
}
public class Animal : Biology
{
public override void Live()
{
Console.WriteLine("Animal重写的抽象方法");
//throw new NotImplementedException();
}
}
Remarque :Les méthodes abstraites ne peuvent être définies que dans des classes abstraites. Si elles ne sont pas définies dans une classe abstraite, l'erreur suivante est signalée :

La différence entre les méthodes virtuelles et les méthodes abstraites estComme les classes abstraites ne peuvent pas être instanciées, les méthodes abstraites ne peuvent pas être appelées ; autrement dit, les méthodes abstraites ne peuvent jamais être implémentées.
5, Méthode de masquageUne méthode définie dans une classe dérivée avec le même nom qu'une méthode de la classe de base, définie à l'aide du mot-clé new. Par exemple, dans la classe de base Animal il y a une méthode Sleep() :
public void Sleep()
{
Console.WriteLine("Animal Sleep");
}
Ensuite, le code pour définir la méthode de masquage dans la classe dérivée Cat est :
new public void Sleep()
{
Console.WriteLine("Cat Sleep");
}
Ou :
public new void Sleep()
{
Console.WriteLine("Cat Sleep");
}
Remarque :
- (1) Une méthode de masquage peut masquer non seulement les méthodes virtuelles de la classe de base, mais aussi les méthodes non virtuelles de la classe de base.
- (2) Pour une méthode de masquage, une instance de la classe parente appelle la méthode de la classe parente, et une instance de la classe enfant appelle la méthode de la classe enfant.
- (3) Comparé au point précédent : Pour une méthode qui redéfinit, une variable de la classe enfant appelle la méthode redéfinie de la classe enfant. Pour une variable de la classe parente, cela dépend si la référence de la classe parente pointe vers une instance de la classe enfant ou vers sa propre instance. Si elle référence une instance de la classe parente, la méthode de la classe de base est appelée ; si elle référence une instance de la classe dérivée, la méthode de la classe dérivée est appelée.
Bon, les concepts de base sont terminés. Regardons un exemple. D'abord, nous créons plusieurs classes :
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();
}
}
Maintenant, regardons le code à exécuter :
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();
}
}
Tout d'abord, nous définissons plusieurs instances des classes que nous devons utiliser. Il convient de noter :
- (1) La classe Biology est une classe abstraite et ne peut pas être instanciée ;
- (2) La variable ac est une instance d'Animal, mais elle pointe vers un objet Cat. Comme Cat est une classe dérivée d'Animal, cette conversion est correcte. C'est aussi le point clé du polymorphisme.
Ensuite, analysons étape par étape :
1,
//重载 a.Sleep(); a.Sleep(23);
Évidemment, les deux méthodes Sleep appelées par la variable a de type Animal sont des méthodes surchargées. La première instruction appelle la méthode Sleep() sans paramètre, et la seconde instruction appelle la méthode Sleep avec un paramètre int. Notez que les types de retour des deux méthodes Sleep sont différents, ce qui illustre aussi la dernière des trois caractéristiques de la redéfinition — le type de retour peut être différent.
Le résultat d'exécution est le suivant :
Animal Sleep Animal在23点Sleep
2,
//重写和虚方法 a.EatFood(); ac.EatFood(); ad.EatFood();
Dans cette section, a, ac et ad sont toutes des instances d'Animal, mais elles référencent des objets différents. a référence un objet Animal, ac référence un objet Cat et ad référence un objet Dog. Quelle différence cela fera-t-il dans les résultats d'exécution ? Voir les résultats d'exécution ci-dessous :
Animal EatFood Cat EatFood Dog EatFood
La première instruction : une instance d'Animal appelle directement la méthode virtuelle EatFood d'Animal, aucun problème.
Dans les deuxième et troisième instructions, bien qu'elles soient aussi des instances d'Animal, elles pointent respectivement vers des objets Cat et Dog, de sorte que les méthodes EatFood redéfinies dans les classes Cat et Dog sont appelées, exactement comme si des instances de Cat et Dog appelaient directement EatFood. C'est l'incarnation du polymorphisme : la même opération appliquée à différents objets peut avoir différentes interprétations et produire différents résultats d'exécution.
3,
//抽象方法 a.Live();
C'est relativement simple. Il redéfinit directement la méthode Live de la classe parente Biology. Le résultat d'exécution est le suivant :
Animal重写的Live
4,
//隐藏方法 a.Sleep(); ac.Sleep(); c.Sleep();
Lors de l'analyse de la méthode de masquage, il convient de la comparer aux méthodes virtuelles et à la redéfinition. Il n'y a pas d'objection à ce que la variable a appelle la méthode Sleep d'Animal et que la variable c appelle la méthode Sleep de Cat. Mais la variable ac référence un objet de type Cat. Doit-elle appeler la méthode EatFood de type Animal ou la méthode EatFood de type Cat ? La réponse est qu'elle appelle la classe parente, à savoir la méthode EatFood d'Animal. Le résultat d'exécution est le suivant :
Animal Sleep Animal Sleep Cat Sleep
La plupart des articles s'arrêtent ici, se contentant de nous faire connaître ces concepts et l'appel de méthodes, sans expliquer pourquoi il en est ainsi. Ensuite, allons un peu plus loin et parlons du mécanisme qui sous-tend le polymorphisme.
2. Compréhension approfondie du polymorphisme
Pour bien comprendre le polymorphisme, il faut commencer par les types valeur et les types référence. Nous savons tous que les types valeur sont stockés sur la pile du thread, tandis que les types référence sont stockés dans le tas managé. Comme toutes les classes sont des types référence, nous ne nous intéressons qu'aux types référence.
Revenons maintenant à l'exemple précédent. Main est le point d'entrée du programme. Lorsque le compilateur JIT compile la fonction Main en instructions natives du CPU, il constate que cette méthode fait référence aux classes Biology, Animal, Cat et Dog, donc le CLR crée plusieurs instances pour représenter ces types eux-mêmes. Nous les appelons « objets de type ». Cet objet contient les champs statiques de la classe, une table de méthodes contenant toutes les méthodes de la classe, ainsi que deux membres supplémentaires que tous les objets du tas managé doivent posséder : le Type Object Pointer et le Sync Block Index.
Le passage ci-dessus peut ne pas être compris par certains amis qui n'ont pas lu de livres pertinents sur le CLR, alors dessinons un schéma pour le décrire :

Le schéma ci-dessus montre ce que le CLR fait avant l'exécution de la fonction Main. Ensuite, commençons l'exécution de la fonction Main (pour plus de commodité, simplifions la fonction Main) :
//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();
Maintenant, instancions trois instances d'Animal, mais elles pointent en fait respectivement vers un objet Animal, un objet Cat et un objet Dog, comme illustré ci-dessous :

Veuillez noter que bien que les variables ac et ad soient toutes deux de type Animal, elles pointent respectivement vers un objet Cat et un objet Dog. C'est là le point clé.
Lors de l'exécution de a.Sleep(), comme Sleep est une méthode d'instance non virtuelle, le compilateur JIT trouvera l'objet de type (objet de type Animal) correspondant au type (Animal) de la variable (a) qui a effectué l'appel. Il appelle ensuite la méthode Sleep dans cet objet de type. Si cet objet de type ne possède pas de méthode Sleep, le compilateur JIT remonte dans les classes de base de la classe (jusqu'à Object) pour trouver la méthode Sleep.
Lors de l'exécution de ac.EatFood, comme EatFood est une méthode d'instance virtuelle, le compilateur JIT génère du code supplémentaire dans la méthode lors de l'appel. Ce code vérifie d'abord la variable (ac) qui a effectué l'appel, puis suit l'adresse de référence de la variable pour trouver l'objet qui a effectué l'appel (objet Cat), trouve l'objet de type correspondant à l'objet qui a effectué l'appel (objet de type Cat), et cherche enfin la méthode EatFood dans cet objet de type. De même, si la méthode EatFood n'est pas trouvée dans cet objet de type, le compilateur JIT remonte vers la classe de base de cet objet de type pour la trouver.
Ce qui est décrit ci-dessus correspond aux différentes méthodes d'exécution du compilateur JIT lorsqu'il rencontre des appels à des méthodes d'instance non virtuelles et à des méthodes d'instance virtuelles. C'est ce traitement différent de ces deux types de méthodes qui produit l'une des trois caractéristiques de la programmation orientée objet que nous voyons en surface : le polymorphisme.
URL d'origine : https://www.cnblogs.com/zhangkai2237/archive/2012/12/20/2826734.html