Polimorfismo

Concepto de polimorfismo

    El polimorfismo es otro de los pilares fundamentales de la programación orientada a objetos. Es la capacidad de almacenar objetos de un determinado tipo en variables de tipos antecesores del primero a costa, claro está, de sólo poderse acceder a través de dicha variable a los miembros comunes a ambos tipos. Sin embargo, las versiones de los métodos virtuales a las que se llamaría a través de esas variables no serían las definidas como miembros del tipo de dichas variables, sino las definidas en el verdadero tipo de los objetos que almacenan.

    A continuación se muestra un ejemplo de cómo una variable de tipo Persona puede usarse para almacenar objetos de tipo Trabajador. En esos casos el campo Sueldo del objeto referenciado por la variable no será accesible, y la versión del método Cumpleaños() a la que se podría llamar a través de la variable de tipo Persona sería la definida en la clase Trabajador, y no la definida en Persona:


using System;
class Persona
{
 // Campo de cada objeto Persona que almacena su nombre
 public string Nombre;     
 // Campo de cada objeto Persona que almacena su edad
 public int Edad;              
 // Campo de cada objeto Persona que almacena su NIF
 public string NIF;            
 
 // Incrementa en uno la edad del objeto Persona
 public virtual void Cumpleaños()
 {
   Console.WriteLine("Incrementada edad de persona");
 }
 
 // Constructor de Persona
 public Persona (string nombre, int edad, string nif)
 {
   Nombre = nombre;
   Edad = edad;
   NIF = nif;
 }
}

class Trabajador: Persona
{
 // Campo de cada objeto Trabajador que almacena cuánto gana
 int Sueldo;
 
 Trabajador(string nombre, int edad, string nif, int sueldo)
     : base(nombre, edad, nif)
 {// Inicializamos cada Trabajador en base al constructor de Persona
   Sueldo = sueldo;
 }
 
 public override Cumpleaños()
 {
   Edad++;
   Console.WriteLine("Incrementada edad de trabajador");
 }
 
 public static void Main()
 {
   Persona p = new Trabajador("Josan", 22, "77588260-Z", 100000);
   p.Cumpleaños();     
   // p.Sueldo++; //ERROR: Sueldo no es miembro de Persona
 }
}

    El mensaje mostrado por pantalla al ejecutar este método confirma lo antes dicho respecto a que la versión de Cumpleaños() a la que se llama, ya que es:

 

 Incrementada edad de trabajador

Métodos genéricos

El polimorfismo es muy útil ya que permite escribir métodos genéricos que puedan recibir parámetros que sean de un determinado tipo o de cualquiera  de sus tipos hijos. Es más, en tanto que cómo se verá en el epígrafe siguiente, en C# todos los tipos derivan implícitamente del tipo System.Object, podemos escribir métodos que admitan parámetros de cualquier tipo sin más que definirlos como métodos que tomen parámetros de tipo System.Object. Por ejemplo:


public void MétodoGenérico(object o)
{
  // Código del método
}

    Nótese que en vez de System.Object se ha escrito object, que es el nombre abreviado incluido en C# para hacer referencia de manera compacta a un tipo tan frecuentemente usado como System.Object.

Determinación de tipo. Operador is

Dentro de una rutina polimórifica que, como la del ejemplo anterior, admita parámetros que puedan ser de cualquier tipo, muchas veces es conveniente poder consultar en el código de la misma cuál es el tipo en concreto del parámetro que se haya pasado al método en cada llamada al mismo. Para ello C# ofrece el operador is, cuya forma sintaxis de uso es:


<expresión> is <nombreTipo>

Este operador devuelve true en caso de que el resultado de evaluar <expresión> sea del tipo cuyo nombre es <nombreTipo> y false en caso contrario[8]. Gracias a ellas podemos escribir métodos genéricos que puedan determinar cuál es el tipo que tienen los parámetros que en cada llamada en concreto se les pasen. O sea, métodos como:


public void MétodoGenérico(object o)
{
 if  (o is int)// Si o es de tipo int (entero)...
               //...Código a ejecutar si el objeto o es de tipo int
 else if (o is string) // Si no, si o es de tipo string (cadena)...
                       //...Código a ejecutar si o es de tipo string
 //... Ídem para otros tipos
}

    El bloque if...else es una instrucción condicional que permite ejecutar un código u otro en función de si la condición indicada entre paréntesis tras el if es cierta (true) o no (false) Esta instrucción se explicará más detalladamente en el Tema 16: Instrucciones

Acceso a la clase base

    Hay determinadas circunstancias en las que cuando redefinamos un determinado método nos interese poder acceder al código de la versión original. Por ejemplo, porque el código redefinido que vayamos a escribir haga lo mismo que el original y además algunas cosas extras. En estos casos se podría pensar que una forma de conseguir esto sería convirtiendo el objeto actual al tipo del método a redefinir y entonces llamar así a ese método, como por ejemplo en el siguiente código:


using System;
class A
{
 public virtual void F()
 {
    Console.WriteLine("A");
 }
}
class B:A
{
 public override void F()
 {
    Console.WriteLine("Antes");
     ((A) this).F();   // (2)
     Console.WriteLine("Después");
 }
 public static void Main()
 {
     B b = new B();
     b.F();
 }
}

Pues bien, si ejecutamos el código anterior veremos que la aplicación nunca termina de ejecutarse y está constantemente mostrando el mensaje Antes por pantalla. Esto se debe a que debido al polimorfismo se ha entrado en un bucle infinito: aunque usemos el operador de conversión para tratar el objeto como si fuese de tipo A, su verdadero tipo sigue siendo B, por lo que la versión de F() a la que se llamará en (2) es a la de B de nuevo, que volverá a llamarse así misma una y otra vez de manera indefinida.

Para solucionar esto, los diseñadores de C# han incluido una palabra reservada llamada base que devuelve una referencia al objeto actual semejante a this pero con la peculiaridad de que los accesos a ella son tratados como si el verdadero tipo fuese el de su clase base. Usando base, podríamos reemplazar el código de la redefinición de F() de ejemplo anterior por:


public override void F()
{
    Console.WriteLine("Antes");
    base.F();
    Console.WriteLine("Después");
}

Si ahora ejecutamos el programa veremos que ahora sí que la versión de F() en B llama a la versión de F() en A, resultando la siguiente salida por pantalla:

 Antes
 A
 Después 

         

A la hora de redefinir métodos abstractos hay que tener cuidado con una cosa: desde el método redefinidor no es posible usar base para hacer referencia a métodos abstractos de la clase padre, aunque sí para hacer referencia a los no abstractos. Por ejemplo:


abstract class A
{
 public abstract void F();
 public void G(){}
}
class B: A
{
 public override void F() 
 {
     base.G();// Correcto
     base.F();// Error, base.F() es abstracto
 }
}

Downcasting

Dado que una variable de un determinado tipo puede estar en realidad almacenando un objeto que sea de algún tipo hijo del tipo de la variable y en ese caso a través de la variable sólo puede accederse a aquellos miembros del verdadero tipo del objeto que sean comunes con miembros del tipo de la variable que referencia al objeto, muchas veces nos va a interesar que una vez que dentro de un método genérico hayamos determinado cuál es el verdadero tipo de un objeto (por ejemplo, con el operador is) podamos tratarlo como tal. En estos casos lo que hay es que hacer una conversión del tipo padre al verdadero tipo del objeto, y a esto se le llama downcasting

Para realizar un downcasting una primera posibilidad es indicar preceder la expresión a convertir del tipo en el que se la desea convertir indicado entre paréntesis. Es decir, siguiendo la siguiente sintaxis:


(
<tipoDestino>) <expresiónAConvertir>

El resultado de este tipo de expresión es el objeto resultante de convertir el resultado de <expresiónAConvertir> a <tipoDestino>. En caso de que la conversión no se pudiese realizar se lanzaría una excepción del tipo predefinido System.InvalidCastException

Otra forma de realizar el downcasting es usando el operador as, que se usa así:


<expresiónAConvertir> as <tipoDestino>

    La principal diferencia de este operador con el anterior es que si ahora la conversión no se pudiese realizar se devolvería null en lugar de lanzarse una excepción. La otra diferencia es que as sólo es aplicable a tipos referencia y sólo a conversiones entre tipos de una misma jerarquía (de padres a hijos o viceversa)

    Los errores al realizar conversiones de este tipo en métodos genéricos se producen cuando el valor pasado a la variable genérica no es ni del tipo indicado en <tipoDestino> ni existe ninguna definición de cómo realizar la conversión a ese tipo (cómo definirla se verá en el Tema 11: Redefinición de operadores)

Clases y métodos sellados

    Una clase sellada es una clase que no puede tener clases hijas, y para definirla basta anteponer el modificador sealed a la definición de una clase normal. Por ejemplo:


sealed class ClaseSellada
{}

    Una utilidad de definir una clase como sellada es que permite que las llamadas a sus métodos virtuales heredados se realicen tan eficientemente como si fuesen no virtuales, pues al no poder existir clases hijas que los redefinan no puede haber polimorfismo y no hay que determinar cuál es la versión correcta del método a la que se ha de llamar.  Nótese que se ha dicho métodos virtuales heredados, pues lo que no se permite es definir miembros virtuales dentro de este tipo de clases, ya que al no poderse heredarse de ellas es algo sin sentido en tanto que nunca podrían redefinirse.

    Ahora bien, hay que tener en cuenta que sellar reduce enormemente su capacidad de reutilización, y eso es algo que el aumento de eficiencia obtenido en las llamadas a sus métodos virtuales no suele compensar. En realidad la principal causa de la inclusión de estas clases en C# es que permiten asegurar que ciertas clases críticas nunca podrán tener clases hijas y sus variables siempre almacenarán objetos del mismo tipo. Por ejemplo, para simplificar el funcionamiento del CLR y los compiladores se ha optado por hacer que todos los tipos de datos básicos excepto System.Object estén sellados.

     Téngase en cuenta que es absurdo definir simultáneamente una clase como abstract y sealed, pues nunca podría accederse a la misma al no poderse crear clases hijas suyas que definan sus métodos abstractos. Por esta razón, el compilador considera erróneo definir una clase con ambos modificadores a la vez.

     Aparte de para sellar clases, también se puede usar sealed como modificador en la redefinición de un método para conseguir que la nueva versión del mismo que se defina deje de ser virtual y se le puedan aplicar las optimizaciones arriba comentadas. Un ejemplo de esto es el siguiente:


class A
{
    public abstract F();
}

class B:A
{
    public sealed override F() // F() deja de ser redefinible
    {}
}

Polimorfismo
José Antonio González Seco

José Antonio es experto en tecnologias Microsoft. Imparte cursos y conferencias en congresos sobre C# y .NET en Universidades de toda España (Sevilla, Barcelona, San Sebastián, Valencia, Oviedo, etc.) en representación de grandes empresas como Microsoft.
Fecha de alta:03/10/2006
Última actualizacion:03/10/2006
Visitas totales:78135
Valorar el contenido:
Últimas consultas realizadas en los foros
Últimas preguntas sin contestar en los foros de devjoker.com