Creo que todos están familiarizados con las tres características de la programación orientada a objetos: encapsulación, herencia y polimorfismo. Todos pueden decir algo al respecto, pero la mayoría solo sabe qué son, sin saber cómo se implementan internamente en el CLR. Por lo tanto, este artículo se centra principalmente en algunos conceptos del polimorfismo y el mecanismo de su implementación interna.
1. Concepto de polimorfismo
Primero, explicaré qué es el polimorfismo: la misma operación aplicada a diferentes objetos puede tener diferentes interpretaciones y producir diferentes resultados de ejecución; esto es el polimorfismo. En otras palabras, en realidad es cuando una instancia del mismo tipo llama a métodos "iguales", pero el resultado producido es diferente. La palabra "iguales" está entre comillas porque estos métodos iguales solo parecen iguales, pero en realidad los métodos que se invocan son diferentes.
Al hablar de polimorfismo, no podemos evitar mencionar los siguientes conceptos: sobrecarga, anulación, métodos virtuales, métodos abstractos y métodos ocultos. A continuación, presentaré sus conceptos uno por uno.
1, Sobrecarga (overload): Dos o más métodos dentro del mismo ámbito (generalmente una clase) con el mismo nombre de función pero diferentes listas de parámetros se denominan sobrecarga. Tienen tres características (conocidas popularmente como dos obligaciones y una opción):
- El nombre del método debe ser el mismo.
- La lista de parámetros debe ser diferente.
- El tipo de retorno puede ser diferente.
Por ejemplo:
public void Sleep()
{
Console.WriteLine("Animal睡觉");
}
public int Sleep(int time)
{
Console.WriteLine("Animal{0}点睡觉", time);
return time;
}
2, Anulación (override): En la subclase, para satisfacer sus propias necesidades, se redefine una implementación diferente de un método; se necesita la palabra clave override. El método anulado debe ser un método virtual y se usa la palabra clave virtual. Sus características son (tres igualdades):
- El mismo nombre de método.
- La misma lista de parámetros.
- El mismo valor de retorno.
Por ejemplo: la definición en la clase principal:
public virtual void EatFood()
{
Console.WriteLine("Animal吃东西");
}
La definición en la subclase:
public override void EatFood()
{
Console.WriteLine("Cat吃东西");
//base.EatFood();
}
Pequeño consejo:Muchos estudiantes suelen preguntar por la diferencia entre sobrecarga y anulación, y en Internet se considera esta diferencia como una de las preguntas comunes de entrevistas de C#. En realidad, estos dos conceptos no tienen ninguna relación; solo comparten el carácter "reiniciar" (que significa "repetir") en chino. No tiene sentido compararlos; basta con distinguir sus definiciones diferentes.
3, Método virtual: Es un método definido en la clase base que permite ser anulado en las clases derivadas, y se define con la palabra clave virtual. Por ejemplo:
public virtual void EatFood()
{
Console.WriteLine("Animal吃东西");
}
Atención: Los métodos virtuales también se pueden llamar directamente. Por ejemplo:
Animal a = new Animal(); a.EatFood();
El resultado de la ejecución es:
Animal吃东西
4, Método abstracto: Es un método definido en la clase base y que debe anularse en la clase derivada, usandoabstractla palabra clave para definirlo. Por ejemplo:
public abstract class Biology
{
public abstract void Live();
}
public class Animal : Biology
{
public override void Live()
{
Console.WriteLine("Animal重写的抽象方法");
//throw new NotImplementedException();
}
}
Atención:Los métodos abstractos solo pueden definirse en clases abstractas. Si no se definen en una clase abstracta, se producirá el siguiente error:

La diferencia entre los métodos virtuales y los métodos abstractos es: Como las clases abstractas no se pueden instanciar, no hay manera de llamar a los métodos abstractos, es decir, los métodos abstractos nunca pueden implementarse.
5, Método oculto: Es un método definido en la clase derivada con el mismo nombre que algún método de la clase base, y se define con la palabra clave new. Por ejemplo, en la clase base Animal hay un método Sleep():
public void Sleep()
{
Console.WriteLine("Animal Sleep");
}
Entonces, el código para definir el método oculto en la clase derivada Cat es:
new public void Sleep()
{
Console.WriteLine("Cat Sleep");
}
O también:
public new void Sleep()
{
Console.WriteLine("Cat Sleep");
}
Atención:
- (1) El método oculto no solo puede ocultar métodos virtuales de la clase base, sino también métodos no virtuales de la clase base.
- (2) En el método oculto, una instancia de la clase principal llama a los métodos de la clase principal, y una instancia de la subclase llama a los métodos de la subclase.
- (3) En contraste con el punto anterior: en la anulación, la variable de la subclase llama al método anulado de la subclase; la variable de la clase principal depende de si esta referencia de la clase principal apunta a una instancia de la subclase o a una instancia de sí misma. Si apunta a una instancia de la clase principal, se llama al método de la clase base; si apunta a una instancia de la clase derivada, se llama al método de la clase derivada.
Bien, hemos terminado con los conceptos básicos. A continuación, veamos un ejemplo. Primero, creamos varias clases:
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();
}
}
A continuación, veamos el código que se necesita ejecutar:
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();
}
}
Primero, definimos varias instancias de las clases que necesitamos usar. Cabe señalar que:
- (1) La clase Biology es abstracta y no se puede instanciar;
- (2) La variable ac es una instancia de Animal, pero apunta a un objeto Cat. Dado que el tipo Cat es una clase derivada del tipo Animal, esta conversión no tiene problema. Este es también el punto clave del polimorfismo.
A continuación, analicémoslo paso a paso:
1,
//重载 a.Sleep(); a.Sleep(23);
Obviamente, los dos métodos Sleep llamados por la variable a de Animal son métodos sobrecargados. La primera llamada es al método Sleep() sin parámetros, y la segunda al método Sleep con un parámetro int. Observe que los valores de retorno de los dos métodos Sleep son diferentes, lo que también ilustra la última de las tres características de la sobrecarga: el valor de retorno puede ser diferente.
El resultado de la ejecución es el siguiente:
Animal Sleep Animal在23点Sleep
2,
//重写和虚方法 a.EatFood(); ac.EatFood(); ad.EatFood();
En este segmento, a, ac y ad son todas instancias de Animal, pero hacen referencia a objetos diferentes: a hace referencia a un objeto Animal, ac a un objeto Cat y ad a un objeto Dog. ¿Qué diferencia produce esta diferencia en los resultados? Vea los resultados de ejecución:
Animal EatFood Cat EatFood Dog EatFood
La primera declaración, una instancia de Animal, llama directamente al método virtual EatFood de Animal, sin ningún problema.
En las declaraciones segunda y tercera, aunque también son instancias de Animal, apuntan a objetos Cat y Dog respectivamente, por lo que llaman a los métodos EatFood anulados en las clases Cat y Dog, como si una instancia de Cat y una instancia de Dog llamaran directamente al método EatFood. Esto es precisamente la manifestación del polimorfismo: la misma operación aplicada a objetos diferentes puede tener diferentes interpretaciones y producir diferentes resultados de ejecución.
3,
//抽象方法 a.Live();
Esto es relativamente simple: simplemente se anula directamente el método Live de la clase principal Biology. El resultado de la ejecución es el siguiente:
Animal重写的Live
4,
//隐藏方法 a.Sleep(); ac.Sleep(); c.Sleep();
Al analizar el método oculto, hay que compararlo con el método virtual y la anulación. No hay duda de que la variable a llama al método Sleep de la clase Animal y la variable c llama al método Sleep de la clase Cat. Pero la variable ac hace referencia a un objeto de tipo Cat: ¿debería llamar al método Sleep del tipo Animal o al método Sleep del tipo Cat? La respuesta es que llama al método Sleep de la clase principal, es decir, Animal. El resultado de la ejecución es el siguiente:
Animal Sleep Animal Sleep Cat Sleep
La mayoría de los artículos llegan hasta aquí, solo nos hacen conocer estos conceptos y los métodos llamados, pero no explican por qué ocurre así. A continuación, profundicemos un poco y hablemos sobre el mecanismo detrás del polimorfismo.
2. Comprensión profunda del polimorfismo
Para comprender profundamente el polimorfismo, debemos comenzar hablando de los tipos de valor y los tipos de referencia. Todos sabemos que los tipos de valor se almacenan en la pila de subprocesos, mientras que los tipos de referencia se almacenan en el montón administrado. Como todas las clases son tipos de referencia, solo nos ocupamos de los tipos de referencia.
Ahora volvamos al ejemplo anterior. La función Main es el punto de entrada del programa. Cuando el compilador JIT compila la función Main a código de CPU nativo, encuentra que este método hace referencia a las clases Biology, Animal, Cat y Dog, por lo que el CLR crea varias instancias para representar esos tipos en sí. A esto lo llamamos "objeto de tipo". Este objeto contiene los campos estáticos de la clase, así como una tabla de métodos que incluye todos los métodos de la clase, y también contiene dos miembros adicionales que todos los objetos en el montón administrado deben tener: el puntero de objeto de tipo (Type Object Pointer) y el índice de bloque de sincronización (sync Block Index).
Puede que el párrafo anterior no sea comprensible para algunos estudiantes que no han leído libros relacionados con CLR, así que dibujemos un diagrama para describirlo:

El diagrama anterior muestra lo que el CLR hace antes de ejecutar la función Main. A continuación, comenzamos a ejecutar la función Main (por conveniencia, simplificamos la función 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();
A continuación, se instancian tres instancias de Animal, pero en realidad apuntan respectivamente a un objeto Animal, un objeto Cat y un objeto Dog, como se muestra a continuación:

Tenga en cuenta que aunque las variables ac y ad son de tipo Animal, apuntan a objetos Cat y Dog respectivamente; este es el punto clave.
Cuando se ejecuta a.Sleep(), dado que Sleep es un método de instancia no virtual, el compilador JIT encontrará el objeto de tipo correspondiente al tipo de la variable que realiza la llamada (a), es decir, el objeto de tipo Animal. Luego llamará al método Sleep en ese objeto de tipo. Si ese objeto de tipo no tiene el método Sleep, el compilador JIT recorrerá las clases base (hasta Object) buscando el método Sleep.
Cuando se ejecuta ac.EatFood, dado que EatFood es un método de instancia virtual, el compilador JIT generará algo de código adicional en el método al realizar la llamada. Este código primero verificará la variable que realiza la llamada (ac), luego seguirá la dirección de referencia de la variable para encontrar el objeto que realiza la llamada (objeto Cat), encontrará el objeto de tipo correspondiente al objeto que realiza la llamada (objeto de tipo Cat) y, finalmente, buscará el método EatFood en ese objeto de tipo. Del mismo modo, si no se encuentra el método EatFood en ese objeto de tipo, el compilador JIT recorrerá las clases base de ese objeto de tipo para buscar el método EatFood.
Lo descrito anteriormente es la forma diferente en que el compilador JIT ejecuta los métodos de instancia no virtuales y los métodos de instancia virtuales al encontrar una llamada. Esta diferencia en el manejo de los dos tipos de métodos es la que produce aparentemente una de las tres características de la programación orientada a objetos que vemos: el polimorfismo.
Dirección original: https://www.cnblogs.com/zhangkai2237/archive/2012/12/20/2826734.html