Polimorfismo en C#

Introducción

El polimorfismo es uno de los cuatro pilares fundamentales de la programación orientada a objetos (POO), junto con la herencia, la encapsulación y la abstracción.

En este artículo, comenzaremos explorando los conceptos fundamentales de la programación orientada a objetos en C#. Luego, profundizaremos en la definición y explicación del polimorfismo, sus tipos y cómo aplicarlos en el desarrollo de software.

También discutiremos cómo el polimorfismo se relaciona con las interfaces y el principio de sustitución de Liskov. Al final del artículo, tendrá una comprensión sólida del polimorfismo en C# y cómo puede utilizar este concepto clave para mejorar sus proyectos de software.

¿Qué es el polimorfismo en C#?

El polimorfismo es un concepto clave en la programación orientada a objetos que permite a los objetos de diferentes clases ser tratados como objetos de una clase común.

En otras palabras, el polimorfismo permite a los desarrolladores utilizar una única interfaz para representar diferentes tipos de datos y objetos. En C#, el polimorfismo se logra a través de la herencia, las interfaces y la sobrecarga de métodos y operadores.

Un ejemplo simple de polimorfismo sería tener una clase base llamada “Forma” y varias clases derivadas como “Círculo”, “Rectángulo” y “Triángulo”.

Aunque cada una de estas clases derivadas tiene propiedades y métodos específicos, todas ellas pueden ser tratadas como objetos de la clase “Forma”. Esto permite a los desarrolladores escribir código más genérico y reutilizable.


using System;

public abstract class Forma
{
    public abstract double Area();
    public abstract double Perimetro();
}

public class Circulo : Forma
{
    public double Radio { get; set; }

    public Circulo(double radio)
    {
        Radio = radio;
    }

    public override double Area()
    {
        return Math.PI * Radio * Radio;
    }

    public override double Perimetro()
    {
        return 2 * Math.PI * Radio;
    }
}

public class Rectangulo : Forma
{
    public double Ancho { get; set; }
    public double Alto { get; set; }

    public Rectangulo(double ancho, double alto)
    {
        Ancho = ancho;
        Alto = alto;
    }

    public override double Area()
    {
        return Ancho * Alto;
    }

    public override double Perimetro()
    {
        return 2 * (Ancho + Alto);
    }
}

public class Triangulo : Forma
{
    public double Base { get; set; }
    public double Altura { get; set; }

    public Triangulo(double baseTriangulo, double altura)
    {
        Base = baseTriangulo;
        Altura = altura;
    }

    public override double Area()
    {
        return 0.5 * Base * Altura;
    }

    public override double Perimetro()
    {
        // No se puede calcular el perímetro sin conocer los otros dos lados del triángulo.
        // Podría agregarse en un constructor si se conoce la longitud de los tres lados.
        throw new NotImplementedException();
    }
}

class Program
{
    static void Main(string[] args)
    {
        Forma[] formas = new Forma[3];
        formas[0] = new Circulo(5);
        formas[1] = new Rectangulo(4, 6);
        formas[2] = new Triangulo(3, 4);

        foreach (Forma forma in formas)
        {
            Console.WriteLine($"Área: {forma.Area()}");

            try
            {
                Console.WriteLine($"Perímetro: {forma.Perimetro()}");
            }
            catch (NotImplementedException)
            {
                Console.WriteLine("Perímetro no disponible para esta forma.");
            }
            Console.WriteLine();
        }
    }
}

Lenguaje del código: C# (cs)

Beneficios del polimorfismo en el desarrollo de software

El polimorfismo en C# ofrece varios beneficios para el desarrollo de software, entre ellos:

  • Reutilización de código: Permite a los desarrolladores escribir código genérico que puede funcionar con diferentes tipos de objetos. Esto reduce la cantidad de código duplicado y facilita su mantenimiento.
  • Flexibilidad: Facilita la extensión y modificación de la funcionalidad del código sin afectar a otras partes del sistema.
  • Abstracción: Proporciona una forma de ocultar la complejidad de las implementaciones específicas y exponer solo las funcionalidades necesarias a través de una interfaz común.
  • Legibilidad: Mejora la legibilidad del código al permitir que los desarrolladores utilicen nombres de métodos y propiedades más descriptivos y significativos, lo que facilita la comprensión del código por parte de otros desarrolladores.
  • Desacoplamiento: Al utilizar una interfaz común en lugar de depender directamente de implementaciones específicas, el polimorfismo permite un mayor desacoplamiento entre componentes del sistema. Esto hace que el código sea más fácil de modificar y mantener.
  • Facilita la implementación de patrones de diseño: Muchos patrones de diseño ampliamente utilizados, como el patrón de estrategia, el patrón de fábrica y el patrón de adaptador, se basan en el polimorfismo para proporcionar soluciones más escalables y flexibles a problemas comunes en el diseño de software.

Tipos de polimorfismo en C#

En C#, hay dos tipos principales de polimorfismo:

  • Polimorfismo estático: También conocido como sobrecarga, el polimorfismo estático ocurre en tiempo de compilación.
  • Polimorfismo dinámico: También conocido como sobrescritura, el polimorfismo dinámico ocurre en tiempo de ejecución.

Polimorfismo estático (sobrecarga) en C#

La sobrecarga es una forma de polimorfismo estático en la que una clase tiene múltiples métodos con el mismo nombre pero con diferentes firmas (tipos y/o número de argumentos). La sobrecarga también se aplica a los operadores, permitiendo a los desarrolladores personalizar el comportamiento de los operadores estándar para sus propias clases.

En C#, la sobrecarga de métodos y operadores se resuelve en tiempo de compilación, lo que significa que el compilador determina qué versión del método u operador se debe llamar basándose en la firma y los argumentos proporcionados.

Ejemplo de sobrecarga de métodos

Aquí hay un ejemplo de sobrecarga de métodos en una clase “Calculadora”:


class Calculadora
{
    public int Suma(int a, int b)
    {
        return a + b;
    }

    public double Suma(double a, double b)
    {
        return a + b;
    }
}

class Program
{
    static void Main()
    {
        Calculadora calculadora = new Calculadora();

        int resultadoEntero = calculadora.Suma(3, 4);
        double resultadoDouble = calculadora.Suma(3.5, 4.2);

        Console.WriteLine($"Resultado entero: {resultadoEntero}");
        Console.WriteLine($"Resultado double: {resultadoDouble}");
    }
}

Lenguaje del código: C# (cs)

En este ejemplo, la clase “Calculadora” tiene dos versiones del método “Suma”. Uno acepta dos argumentos enteros y el otro acepta dos argumentos double. El compilador selecciona la versión correcta del método en función de los tipos de argumentos proporcionados.

Ejemplo de sobrecarga de operadores

Aquí hay un ejemplo de sobrecarga del operador “+” para una clase “Vector”:


class Vector
{
    public double X { get; set; }
    public double Y { get; set; }

    public Vector(double x, double y)
    {
        X = x;
        Y = y;
    }

    public static Vector operator +(Vector a, Vector b)
    {
        return new Vector(a.X + b.X, a.Y + b.Y);
    }
}

class Program
{
    static void Main()
    {
        Vector vector1 = new Vector(1, 2);
        Vector vector2 = new Vector(3, 4);

        Vector suma = vector1 + vector2;

        Console.WriteLine($"Suma de vectores: ({suma.X}, {suma.Y})");
    }
}

Lenguaje del código: C# (cs)

En este ejemplo, la clase “Vector” sobrecarga el operador “+” para permitir la suma de dos objetos “Vector”. El operador “+” se define como un método estático con la palabra clave “operator” seguida del símbolo del operador que se sobrecarga.

Reglas y restricciones en la sobrecarga

Hay algunas reglas y restricciones que se deben tener en cuenta al sobrecargar métodos y operadores en C#:

  • No se pueden sobrecargar los métodos solo por su tipo de retorno. Deben tener diferentes números o tipos de argumentos.
  • Los métodos y operadores sobrecargados deben definirse en la misma clase o en una clase derivada.
  • No se pueden sobrecargar todos los operadores en C#. Por ejemplo, los operadores de asignación (como =) y los operadores condicionales (como ?. y ??) no se pueden sobrecargar.
  • Al sobrecargar operadores, es importante mantener la coherencia y la legibilidad en mente. Los operadores sobrecargados deben realizar operaciones similares a las de sus contrapartes nativas.
  • Al sobrecargar operadores binarios, también se recomienda sobrecargar los operadores de igualdad y desigualdad (como == y !=) para garantizar una comparación adecuada entre objetos de la clase.
  • No se pueden sobrecargar directamente los operadores de conversión implícita y explícita (implicit y explicit). En su lugar, se debe proporcionar un método de conversión en la clase destino.

Al tener en cuenta estas reglas y restricciones, la sobrecarga de métodos y operadores en C# puede ser una herramienta poderosa y flexible para personalizar el comportamiento de las clases y mejorar la legibilidad y la eficiencia del código.

Polimorfismo dinámico (sobrescritura) en C#

La sobrescritura es una forma de polimorfismo dinámico en la que una clase derivada redefine un método o propiedad de su clase base.

La sobrescritura permite que la clase derivada proporcione una implementación específica de un método o propiedad mientras mantiene la misma firma que la versión de la clase base.

En tiempo de ejecución, el tipo de objeto determina qué versión del método o propiedad se debe llamar.

Uso de las palabras clave virtual, override y ‘base’

En C#, las palabras clave virtual, override y ‘base’ se utilizan para controlar y gestionar la sobrescritura de métodos y propiedades:

  • virtual: Se utiliza en la clase base para indicar que un método o propiedad puede ser sobrescrito en una clase derivada.
  • override: Se utiliza en la clase derivada para indicar que un método o propiedad está sobrescribiendo un método o propiedad virtual de la clase base.
  • base: Se utiliza en la clase derivada para llamar a la versión del método o propiedad de la clase base que está siendo sobrescrita.

Ejemplo de sobrescritura de métodos

Aquí hay un ejemplo de sobrescritura de métodos utilizando clases “Animal” y “Perro”:


public class Animal
{
    public virtual void HacerSonido()
    {
        Console.WriteLine("El animal hace un sonido");
    }
}

public class Perro : Animal
{
    public override void HacerSonido()
    {
        Console.WriteLine("El perro ladra");
    }
}

class Program
{
    static void Main()
    {
        Animal miAnimal = new Animal();
        Animal miPerro = new Perro();

        miAnimal.HacerSonido(); // Salida: El animal hace un sonido
        miPerro.HacerSonido(); // Salida: El perro ladra
    }
}

Lenguaje del código: C# (cs)

En este ejemplo, la clase “Animal” define un método virtual “HacerSonido”. La clase derivada “Perro” sobrescribe este método con su propia implementación utilizando la palabra clave override.

Cuando se llama al método “HacerSonido” en un objeto “Perro”, se ejecuta la versión sobrescrita del método.

Ejemplo de sobrescritura de propiedades

Aquí hay un ejemplo de sobrescritura de propiedades utilizando clases “Empleado” y “Gerente”:


public class Empleado
{
    public virtual string Descripcion
    {
        get { return "Empleado"; }
    }
}

public class Gerente : Empleado
{
    public override string Descripcion
    {
        get { return "Gerente"; }
    }
}

class Program
{
    static void Main()
    {
        Empleado empleado = new Empleado();
        Empleado gerente = new Gerente();

        Console.WriteLine(empleado.Descripcion); // Salida: Empleado
        Console.WriteLine(gerente.Descripcion); // Salida: Gerente
    }
}

Lenguaje del código: C# (cs)

En este ejemplo, la clase “Empleado” define una propiedad virtual “Descripcion”. La clase derivada “Gerente” sobrescribe esta propiedad con su propia implementación utilizando la palabra clave override. Cuando se accede a la propiedad “Descripcion” en un objeto “Gerente”, se devuelve la versión sobrescrita de la propiedad.

Reglas y restricciones en la sobrescritura

Hay algunas reglas y restricciones que se deben tener en cuenta al sobrescribir métodos y propiedades en C#:

  • Solo se pueden sobrescribir los métodos y propiedades marcados como virtual, abstract o override en la clase base.
  • La firma del método o propiedad sobrescrito en la clase derivada debe ser idéntica a la firma del método o propiedad en la clase base.
  • Un método o propiedad marcado como sealed en la clase base no puede ser sobrescrito en una clase derivada.
  • Un método o propiedad sobrescrito en una clase derivada puede ser marcado como sealed para evitar la sobrescritura adicional en las clases derivadas subsecuentes.
  • Al sobrescribir un método o propiedad, la accesibilidad no puede ser más restrictiva que la accesibilidad del método o propiedad en la clase base. Por ejemplo, un método protected en la clase base no puede ser sobrescrito como private en la clase derivada.

Al tener en cuenta estas reglas y restricciones, la sobrescritura de métodos y propiedades en C# permite a los desarrolladores crear código flexible y fácil de mantener que aprovecha la herencia y el polimorfismo dinámico para adaptarse a las necesidades específicas de sus aplicaciones.

Interfaces en C# y polimorfismo

Las interfaces en C# son una forma de definir contratos para las clases. Una interfaz es una colección de miembros abstractos, como métodos, propiedades, eventos e indexadores, que las clases que implementan la interfaz deben proporcionar.

Las interfaces no pueden contener implementaciones de miembros ni campos. Una clase puede implementar múltiples interfaces, lo que permite el polimorfismo y la reutilización de código en C#.

Ejemplo de implementación de interfaces en C#

Aquí hay un ejemplo de una interfaz llamada “IVolable” y dos clases, “Pajaro” y “Avion”, que implementan la interfaz:


public interface IVolable
{
    void Volar();
}

public class Pajaro : IVolable
{
    public void Volar()
    {
        Console.WriteLine("El pájaro vuela");
    }
}

public class Avion : IVolable
{
    public void Volar()
    {
        Console.WriteLine("El avión vuela");
    }
}

Lenguaje del código: C# (cs)

En este ejemplo, la interfaz “IVolable” define un método “Volar”. Las clases “Pajaro” y “Avion” implementan la interfaz “IVolable” y proporcionan sus propias implementaciones del método “Volar”.

Interfaces y polimorfismo: uso de interfaces para crear objetos polimórficos

Las interfaces también pueden ser utilizadas para implementar el polimorfismo. Puedes crear objetos polimórficos utilizando interfaces en lugar de clases base.

Aquí hay un ejemplo que muestra cómo utilizar la interfaz “IVolable” para crear objetos polimórficos:


class Program
{
    static void Main()
    {
        IVolable miPajaro = new Pajaro();
        IVolable miAvion = new Avion();

        miPajaro.Volar(); // Salida: El pájaro vuela
        miAvion.Volar();  // Salida: El avión vuela
    }
}

Lenguaje del código: C# (cs)

En este ejemplo, se crean objetos de tipo “IVolable” que hacen referencia a instancias de las clases “Pajaro” y “Avion”. Ambos objetos son tratados como objetos de tipo “IVolable”, lo que permite que el código sea más genérico y reutilizable.

Interfaces explícitas e implícitas

En C#, hay dos formas de implementar una interfaz: implícita y explícitamente.

  • Implementación implícita: Cuando una clase implementa una interfaz sin especificar el nombre de la interfaz antes del nombre del miembro, se considera una implementación implícita. Esto permite que el miembro sea invocado tanto a través de la interfaz como a través de la instancia de la clase.

public class Pajaro : IVolable
{
    public void Volar()
    {
        Console.WriteLine("El pájaro vuela");
    }
}

Lenguaje del código: C# (cs)
  • Implementación explícita: Cuando una clase implementa una interfaz especificando el nombre de la interfaz antes del nombre del miembro, se considera una implementación explícita. Esto oculta el miembro cuando se accede a través de la instancia de la clase y solo es accesible cuando se utiliza la referencia de la interfaz.

public class Pajaro : IVolable
{
    void IVolable.Volar()
    {
        Console.WriteLine("El pájaro vuela");
    }
}

Lenguaje del código: C# (cs)

En este ejemplo, la clase “Pajaro” implementa explícitamente la interfaz “IVolable”. El método “Volar” solo es accesible a través de la referencia de la interfaz “IVolable” y no a través de la instancia de la clase “Pajaro”:


class Program
{
    static void Main()
    {
        Pajaro miPajaro = new Pajaro();
        IVolable miVolable = miPajaro;

        // miPajaro.Volar(); // Error de compilación: 'Pajaro' no contiene una definición para 'Volar'
        miVolable.Volar(); // Salida: El pájaro vuela
    }
}

Lenguaje del código: C# (cs)

En este ejemplo, al intentar llamar al método “Volar” en la instancia de “Pajaro” se producirá un error de compilación, ya que la implementación explícita de la interfaz oculta el miembro “Volar” cuando se accede a través de la instancia de la clase.

Sin embargo, al acceder al método “Volar” a través de la referencia de la interfaz “IVolable”, se ejecuta correctamente.

La implementación explícita de interfaces es especialmente útil cuando una clase implementa múltiples interfaces que tienen miembros con el mismo nombre o cuando se desea ocultar miembros de la interfaz en la clase que los implementa.

Polimorfismo y el principio de sustitución de Liskov (LSP)

El principio de sustitución de Liskov (LSP) es uno de los cinco principios SOLID del diseño orientado a objetos y de programación. Este principio establece que los objetos de una clase derivada deberían poder reemplazar los objetos de su clase base sin afectar la corrección del programa.

En otras palabras, si una clase S es un subtipo de una clase T, entonces un objeto de la clase T debería poder ser reemplazado por un objeto de la clase S sin alterar las propiedades deseables del programa.

El LSP garantiza que las clases derivadas sigan el comportamiento esperado de la clase base, lo que lleva a un diseño de software más robusto y fácil de mantener.

Cómo el polimorfismo apoya el LSP en C#

El polimorfismo en C# apoya el principio de sustitución de Liskov al permitir que los objetos de las clases derivadas sean tratados como objetos de su clase base.

Al utilizar la herencia, las clases derivadas pueden sobrescribir o extender el comportamiento de la clase base, lo que permite que las instancias de las clases derivadas sean utilizadas en lugar de las instancias de la clase base sin afectar la corrección del programa.

Ejemplo práctico de LSP y polimorfismo en C#

Aquí hay un ejemplo que ilustra el principio de Liskov utilizando la herencia y el polimorfismo en C#:


public abstract class Ave
{
    public abstract void Comer();

    public void Dormir()
    {
        Console.WriteLine("El ave duerme");
    }
}

public class Pajaro : Ave
{
    public override void Comer()
    {
        Console.WriteLine("El pájaro come semillas");
    }
}

public class Aguila : Ave
{
    public override void Comer()
    {
        Console.WriteLine("El águila come carne");
    }
}

class Program
{
    static void ProcesarAve(Ave ave)
    {
        ave.Comer();
        ave.Dormir();
    }

    static void Main()
    {
        Pajaro pajaro = new Pajaro();
        Aguila aguila = new Aguila();

        ProcesarAve(pajaro); // Salida: El pájaro come semillas \n El ave duerme
        ProcesarAve(aguila); // Salida: El águila come carne \n El ave duerme
    }
}

Lenguaje del código: C# (cs)

En este ejemplo, la clase abstracta “Ave” define dos métodos: “Comer” y “Dormir”. Las clases “Pajaro” y “Aguila” heredan de la clase “Ave” y sobrescriben el método “Comer” para proporcionar su propio comportamiento. El método “ProcesarAve” acepta un parámetro de tipo “Ave” y llama a los métodos “Comer” y “Dormir” en el objeto “Ave” proporcionado.

Cuando se llama a “ProcesarAve” con objetos de las clases “Pajaro” y “Aguila”, el comportamiento de los métodos “Comer” y “Dormir” es el esperado, lo que demuestra que el principio de sustitución de Liskov se cumple en este ejemplo.

Las instancias de las clases derivadas “Pajaro” y “Aguila” pueden ser utilizadas en lugar de instancias de la clase base “Ave” sin afectar la corrección del programa.

Este ejemplo también muestra cómo el polimorfismo en C# permite que las instancias de las clases derivadas sean tratadas como objetos de su clase base. Al utilizar el polimorfismo y seguir el principio de sustitución de Liskov, los desarrolladores pueden escribir código más genérico, reutilizable y fácil de mantener.

Referencias

  1. Documentación oficial de Microsoft sobre Polimorfismo en C#: La documentación oficial de Microsoft proporciona una guía completa sobre el polimorfismo en C# con ejemplos y explicaciones detalladas.