La Escuela del Programador

 

Covarianza y contravarianza

En C# y Visual Basic, la covarianza y la contravarianza permiten la conversión implícita de referencias para los tipos array, los tipos delegate y los argumentos de tipo genérico. Antes de ver un ejemplo del uso de estos conceptos en .Net, vamos a definirlos, tal como se usan en las ciencias computacionales:

En los lenguaje de programación, la covarianza y la contravarianza son conceptos que se refieren a la ordenación de los tipos (del más específico al más general, en la covarianza y a la inversa, o sea del mas general al más específico en la covarianza) y su intercambiabilidad o equivalencia en ciertas situaciones (por ejemplo, los parámetros, genéricos, y los tipos de retorno). Covarianza: asignar un tipo más general (Vehículo) a uno más específico (Auto) ya que todo Auto es un Vehículo. La compatibilidad de asignación se preserva (los tipos se ordenan desde el más específico al más genérico). Contravarianza: asignar un tipo más específico (Perro) al más general (Animal). La compatibilidad de asignación se invierte (los tipos se ordenan desde el más genérico al más específico).

El siguiente código (escrito en C#), demuestra la diferencia entre compatibilidad de asignación, covarianza, y contravarianza: Compatibilidad de asignación:

string str = "test";
// Un objeto de un tipo más específico es asignado a
// un objeto de un tipo más general.
object obj = str;

Covarianza:

IEnumerable<string> listaCadenas = new List<string>();
// Un objeto que es instanciado con un argumento de un tipo más
// específico es asignado a un objeto instanciado con un argumento
// de un tipo más general.
// La compatibilidad de asignación es preservada.
IEnumerable<object> objetos = listaCadenas;

Contravarianza:

// Asumiendo que el método siguiente está en la clase:
// static void SetObject(object o) { }
Action<object> actObject = SetObject;
// Un objeto que es instanciado con un argumento de un tipo
// más general es asignado a un objeto instanciado con un
// argumento de un tipo mas específico.
// La compatibilidad de asignación es invertida.
Action<string>actString = actObject;

Pero para entender mejor todos estos temas, nada mejor que un ejemplo completo de código dónde veremos los siguientes temas:

  1. Uso de la varianza en general (término para referirse tanto a la covarianza como a la contravarianza)
  2. Delegado Action(of T) -en VB- o Action<T> -en C#-
  3. Uso de una expresión lambda
  4. Tipos Genéricos.
  5. Método ForEach del tipo genérico List(T), que admite como parámetro un delegado Action(of T) -en VB- o Action<T> -en C#- y realiza la acción sobre cada elemento de una lista de objetos del tipo T (List<T>)

Creemos una clase Persona, bien simple, como sigue:

    class Persona
    {
        private string nombre;
        public string Nombre
        {
            get { return nombre; }
            set { nombre = value; }
        }

        private byte edad;
        public byte Edad
        {
            get { return edad; }
            set { edad = value; }
        }

        public Persona() { }

        public Persona(string n, byte e)
        {
            this.nombre = n;
            this.edad = e;
        }

        public override string ToString()
        {
            return  String.Format("{0} tiene {1} años",
                                nombre, edad) ;
        }
    }

Como ve, esta clase Persona es bien simple: consta de los siguientes miembros:

  • Campos privados: nombre y edad de los tipos string y byte respectivamente.
  • Propiedades: Nombre y Edad de los mismos tipos que los campos respectivos.
  • Constructores: Dos constructores. El constructor por defecto (sin parámetros) y otro que admite 2 parámetros (el nombre de tipo string y la edad de tipo byte)
  • Métodos: El método ToString() que sobreescribe el método virtual heredado de la clase Object y que tiene por objeto escribir una representación de cadena de un objeto del tipo Persona. Por ejemplo, si tuviéramos un objeto del tipo Persona con un nombre de Mónica y una edad igual a 16, el método ToString() devolvería la cadena "Mónica tiene 16 años."

Ahora construyamos una clase Estudiante que herede de Persona, y hagámosla bien simple también, para una mejor comprensión de los temas explicados:

 class Estudiante:Persona
    {
        private string carrera;
        public string Carrera
        {
            get { return carrera; }
            set { carrera = value; }
        }

        public Estudiante() { }

        public Estudiante(string n, byte e, string c):base(n, e)
        {
            this.carrera = c;
        }

        public override string ToString()
        {
            return base.ToString() +
                 String.Format(" y estudia la carrera {0}",carrera);
        }
    }

Como ve, esta clase Estudiante es tambien muy simple: Hereda de Persona y consta de los siguientes miembros:

    • Campos privados: nombre y edad de los tipos string y byte respectivamente (heredados de Persona) y carrera del tipo string.
    • Propiedades: Las dos heredadas de Persona (Nombre y Edad) y la propiedad Carrera del tipo string.
    • Constructores: Dos constructores. El constructor por defecto (sin parámetros) y otro que admite 3 parámetros (el nombre, de tipo string, la edad de tipo byte y la carrera de tipo string) Es importante observar detenidamente el constructor que admite 3 parámetros: en él, se hace uso de la palabra reservada base (en VB la palabra clave equivalente es MyBase) que especifica que cuando se creen instancias de la clase derivada (Estudiante) se debe llamar al constructor de la clase base (Persona) que admite dos parámetros y luego dentro del cuerpo del constructor se asigna al campo carrera, el valor pasado para el mismo.
    • Métodos: El método ToString() que sobreescribe el método heredado de la clase Persona. Aquí también se hace uso de la palabra clave base, pero con otro objetivo: Llamar al método ToString() en la clase base que está siendo sobreescrito por éste método. De esta manera el siguiente código:
      return base.ToString() +
             String.Format(" y estudia la carrera {0}",carrera);
      Primero llama al método ToString de la clase Persona y a eso le concatena la cadena devuelta por String.Format(" y estudia la carrera {0}",carrera);... lo que, por ejemplo para un objeto Estudiante cuyo nombre es "Marco", su edad es igual a 19 y la carrera es "Abogacía", da como resultado: Marco tiene 19 años (esta cadena es devuelta por el método ToString() en la clase Persona) y estudia la carrera Abogacía.

Ahora construyamos un aplicación de tipo consola, llamándola Ejemplo, con el siguuiente código (los números son sólo para referencia en la explicación que daré mas adelante):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Ejemplo
{
    class Program
    {
     1. private static List<Persona> contactList =
                     new List<Persona>();

        static void Main(string[] args)
        {
          2. Action<Persona> addPersonToContacts = AddToContacts;
          3. addPersonToContacts(new Persona("Miguel", 8));
          4. Action<Estudiante> addEstudianteToContacts =
                       AddToContacts;
          5. addEstudianteToContacts(new Estudiante("Laura",
                       20, "Contabilidad"));
          6. addEstudianteToContacts = addPersonToContacts;
          7. addEstudianteToContacts(new Estudiante("Pedro",
                       40, "Medicina"));
          8. for (int i = 0; i < contactList.Count; i++)
                 Console.WriteLine("Nombre: {0}, Edad: {1} años",
                         contactList[i].Nombre, contactList[i].Edad);

              Console.WriteLine();
              Console.WriteLine("================================");
              Console.WriteLine();
           9. contactList.ForEach(s => Console.WriteLine(s));
        }

    10. static void AddToContacts(Persona person)
        {
              // Este método agrega un objeto Persona
              // a la lista de contactos.
              contactList.Add(person);
        }
    }
}

Explicación del Código

Nuestra clase principal del programa, define en la línea marcada con el número 1, un campo estático llamado contactList que es del tipo List<Persona>, un tipo específico del tipo genérico List<T>, que será nuestra lista de contactos, donde iremos agregando Personas y Estudiantes. En la línea marcada con el 2, creamos una instancia del delegado Action<Persona>, llamada addPersonToContacts, que es un tipo específico del tipo genérico Action<T>, sin usar varianza, o sea que directamente apuntamos nuestro delegado al método AddToContacts, definido en el bloque que comienza en la línea marcada con el 10. Es importante notar que aquí no estamos usando ningún tipo de varianza porque tanto el delegado addPersonToContacts, como el método al que apunta (AddToContacts) esperan un parámetro del tipo Persona.

A partir de Visual Studio 2008 y .Net Framework 3.5, en el namespace System, vienen definidos 17 delegados Func y 17 delegados Action, de la siguiente manera:
  • Func(TResult):
  • Encapsula un método que no tiene parámetros y devuelve un valor del tipo especificado por el parámetro TResult.
  • Func(T, TResult):
  • Encapsula un método que tiene un parámetro y devuelve un valor del tipo especificado por el parámetro TResult.
  • Func(T1, T2, TResult):
  • Encapsula un método que tiene 2 parámetros y devuelve un valor del tipo especificado por el parámetro TResult.
  •  
  • ...
  •  
  • ...
  • Func(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, TResult):
  • Encapsula un método que tiene 16 parámetros y devuelve un valor del tipo especificado por el parámetro TResult.
  • Action:
  • Encapsula un método que no tiene parámetros y no devuelve un valor.
  • Action(T):
  • Encapsula un método que tiene un parámetro y no devuelve un valor.
  • Action(T1, T2):
  • Encapsula un método que tiene 2 parámetros y no devuelve un valor.
  •  
  • ...
  •  
  • ...
  • Action(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16):
  • Encapsula un método que tiene 16 parámetros y no devuelve un valor.

En la línea marcada con 3, usamos el delegado anterior (addPersonToContacts), simplemente llamando al método al que apunta pasándole un objeto Persona, cuyo nombre es Miguel y la edad es igual a 8. Viendo dentro del código del método (que comienza en la línea marcada con 10), observamos que este método simplemente agrega a la lista definida en 1, el objeto pasado como parámetro. En la línea 4, creamos una instancia del delegado Action<Estudiante>, llamada addEstudianteToContacts, que también es un tipo específico del tipo genérico Action<T>. Este nuevo delegado, espera un método que tiene un parámetro Estudiante, pero podemos asignarle (como lo hacemos en la línea 4, un método (AddToContacts) que admite un parámetro Persona, porque Estudiante deriva de Persona. Aquí estamos usando covarianza, ya que la conversión usada es muy natural y luce igual que el Polimorfismo. En la línea 5, simplemente usamos este delegado, llamando al método al que apunta pasándole un objeto Estudiante, cuyo nombre es Laura, su edad es igual a 20 y su carrera es Contabilidad. Viendo dentro del código del método (que comienza en la línea marcada con 10), observamos que este método simplemente agrega a la lista definida en 1, el objeto pasado como parámetro. En la línea 6, asignamos un delegado que acepta un parámetro de tipo más general (addPersonToContacts) a un delegado que acepta un parámetro de un tipo más específico (addEstudianteToContacts). Aquí estamos usando Contravarianza (la compatibilidad de asignación es invertida, , o sea ordena los tipos desde el más genérico al más específico) : En la línea 7, usamos este delegado, pasándole al método que apunta (AddToContacts) un objeto Estudiante, cuyo nombre es Pedro, su edad es igual a 40 y su carrera es Medicina. En la línea 8, recorremos la lista de Personas y escribimos las propiedades de cada persona que la integran Finalmente, en la línea 9, recorremos la lista, usando el método ForEach(), que admite un predicado del tipo Action, lo que devolverá cada representación en cadena de cada objeto que la forma, llamado al metodo ToString(). Como originariamente, le pasamos un objeto del tipo Estudiante, la cadena que lo representa tambien tiene la carrera Observe que el predicado del tipo Action que se le pasa al método ForEach(), está representado por una expresión lambda (un método anónimo que recibe un parámetro de tipo string y escribe en pantalla esa cadena:

  contactList.ForEach(s => Console.WriteLine(s));

Esto es equivalente a este código:

  foreach (var item in contactList)
  {
       Console.WriteLine(item.ToString());
  }

.NET Framework 4 incluye soporte de varianza para varias interfaces genéricas ya existentes. La compatibilidad con la varianza habilita la conversión implícita de las clases que implementan estas interfaces. Para un mayor detalle de este tema vea Varianza en interfaces genéricas (C# y Visual Basic)

respag
Panamá - © 2012
http://respag.net/covarianza-y-contravarianza.aspx