87 - Clases genéricas


C# permite crear clases que administren distintos tipos de datos.

Se utilizan mucho para la administración de colecciones de datos (pilas, colas, listas, árboles etc.)

Para entender las ventajas de definir clases genéricas implementaremos los algoritmos para administrar una pila de enteros y una pila de string. Primero lo haremos utilizando clases tradicionales y luego mediante una clase genérica.

Para concentrarnos en la sintaxis plantearemos la pila utilizando un vector de 5 elementos y definiremos los dos métodos fundamentales de insertar y extraer (no haremos ningún tipo de validaciones por simplicidad)

Programa:

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

namespace SinGenericos
{
    class PilaEnteros
    {
        private int[] vec = new int[5];
        private int tope = 0;

        public void Insertar(int x)
        {
            vec[tope] = x;
            tope++;
        }

        public int Extraer()
        {
            tope--;
            return vec[tope];
        }
    }

    class PilaString
    {
        private string[] vec = new string[5];
        private int tope = 0;

        public void Insertar(string x)
        {
            vec[tope] = x;
            tope++;
        }

        public string Extraer()
        {
            tope--;
            return vec[tope];
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            PilaEnteros pila1 = new PilaEnteros();
            pila1.Insertar(20);
            pila1.Insertar(40);
            pila1.Insertar(17);
            Console.WriteLine(pila1.Extraer());

            PilaString pila2 = new PilaString();
            pila2.Insertar("juan");
            pila2.Insertar("ana");
            pila2.Insertar("luis");
            Console.WriteLine(pila2.Extraer());

            Console.ReadKey();
        }
    }
}

Como podemos analizar hemos planteado dos clases, una para administrar una pila con tipos de dato enteros:

    class PilaEnteros
    {
        private int[] vec = new int[5];
        private int tope = 0;

        public void Insertar(int x)
        {
            vec[tope] = x;
            tope++;
        }

        public int Extraer()
        {
            tope--;
            return vec[tope];
        }
    }

Y por otro lado otra clase para administrar una pila de tipo de dato string:

    class PilaString
    {
        private string[] vec = new string[5];
        private int tope = 0;

        public void Insertar(string x)
        {
            vec[tope] = x;
            tope++;
        }

        public string Extraer()
        {
            tope--;
            return vec[tope];
        }
    }

En la main para probar estas dos clases definimos un objeto de la clase PilaEnteros e insertamos tres valores y luego extraemos uno:

            PilaEnteros pila1 = new PilaEnteros();
            pila1.Insertar(20);
            pila1.Insertar(40);
            pila1.Insertar(17);
            Console.WriteLine(pila1.Extraer()); //se imprime el 17 ya que se trata de una pila.

De forma similar probamos creando un objeto de la clase PilaString:

            PilaString pila2 = new PilaString();
            pila2.Insertar("juan");
            pila2.Insertar("ana");
            pila2.Insertar("luis");
            Console.WriteLine(pila2.Extraer());

Hasta este momento no hemos presentado ninguna novedad con respecto a lo que conocemos. Veamos ahora como podemos resolver este problema pero empleando una clase genérica:

Programa:

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

namespace Genericos1
{
    class Pila<T>
    {
        private T[] vec = new T[5];
        private int tope = 0;

        public void Insertar(T x)
        {
            vec[tope] = x;
            tope++;
        }

        public T Extraer()
        {
            tope--;
            return vec[tope];
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Pila <int>pila1 = new Pila<int>();
            pila1.Insertar(20);
            pila1.Insertar(40);
            pila1.Insertar(17);
            Console.WriteLine(pila1.Extraer());

            Pila<string>pila2 = new Pila<string>();
            pila2.Insertar("juan");
            pila2.Insertar("ana");
            pila2.Insertar("luis");
            Console.WriteLine(pila2.Extraer());

            Console.ReadKey();
        }
    }
}

Como vemos hemos declarado una sola clase llamada Pila y hemos sustituido en los lugares donde hacíamos referencia a int o string por el tipo 'T' que también tenemos que hacer referencia en la primer línea:

    class Pila<T>
    {
        private T[] vec = new T[5];
        private int tope = 0;

        public void Insertar(T x)
        {
            vec[tope] = x;
            tope++;
        }

        public T Extraer()
        {
            tope--;
            return vec[tope];
        }
    }

Luego en la main cuando creamos un objeto de la clase Pila debemos indicar cuando la creamos el tipo de datos que administrará nuestra pila:

            Pila <int>pila1 = new Pila<int>();

Podemos crear objetos de la clase Pila con cualquier tipo de dato primitivo (int, char, float, double etc.) o de otra clase.

Modifiquemos el ejercicio anterior para crear una pila de la clase Persona (almacena el nombre y la edad):

Programa:

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

namespace Genericos2
{
    class Persona
    {
        public string Nombre { get; set; }
        public int Edad { get; set; }
    }

    class Pila<T>
    {
        private T[] vec = new T[5];
        private int tope = 0;

        public void Insertar(T x)
        {
            vec[tope] = x;
            tope++;
        }

        public T Extraer()
        {
            tope--;
            return vec[tope];
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            Pila<Persona> pila1 = new Pila<Persona>();
            Persona persona1 = new Persona { Nombre = "Juan", Edad = 22 };
            Persona persona2 = new Persona { Nombre = "Ana", Edad = 34 };
            Persona persona3 = new Persona { Nombre = "Carlos", Edad = 47 };
            pila1.Insertar(persona1);
            pila1.Insertar(persona2);
            pila1.Insertar(persona3);
            Persona p = pila1.Extraer();
            Console.WriteLine(p.Nombre + " " + p.Edad);
            Console.ReadKey();
        }
    }
}

Declaramos una clase llamada Persona con dos propiedades:

    class Persona
    {
        public string Nombre { get; set; }
        public int Edad { get; set; }
    }

La clase Pila no se modifica en absoluto con respecto al problema anterior:

    class Pila<T>
    {
        private T[] vec = new T[5];
        private int tope = 0;

        public void Insertar(T x)
        {
            vec[tope] = x;
            tope++;
        }

        public T Extraer()
        {
            tope--;
            return vec[tope];
        }
    }

En la main creamos un objeto de la clase Pila e indicamos que almacenará objetos de la clase Persona:

            Pila<Persona> pila1 = new Pila<Persona>();

Creamos tres objetos de la clase Persona y los insertamos en la Pila:

            Persona persona1 = new Persona { Nombre = "Juan", Edad = 22 };
            Persona persona2 = new Persona { Nombre = "Ana", Edad = 34 };
            Persona persona3 = new Persona { Nombre = "Carlos", Edad = 47 };
            pila1.Insertar(persona1);
            pila1.Insertar(persona2);
            pila1.Insertar(persona3);

Extraemos un elemento de la pila y guardamos su referencia en la variable "p" que debe ser de la clase Persona:

            Persona p = pila1.Extraer();
            Console.WriteLine(p.Nombre + " " + p.Edad);

Como vemos el planteo de clases genéricas nos reduce tener que crear múltiples clases para administrar distintos tipos de datos.

Problema:

Plantear una clase para administrar una lísta de datos utilizando genéricos. Implemente los métodos para insertar, extraer, cantidad e imprimir.

Crear luego tres objetos de la clase Lista uno con enteros, otros con string y finalmente otro de tipo Persona (declarar una clase Persona con dos propiedades: Nombre y Edad)

Programa:

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

namespace Genericos2
{
    class Persona
    {
        public string Nombre { get; set; }
        public int Edad { get; set; }

        public override string ToString()
        {
            return "(" + Nombre + "-" + Edad + ")";
        }
    }


    class ListaGenerica<T>
    {
        class Nodo
        {
            public T Info { get; set; }
            public Nodo Sig { get; set; }
        }

        private Nodo raiz;

        public ListaGenerica()
        {
            raiz = null;
        }

        public void Insertar(int pos, T x)
        {
            if (pos <= Cantidad() + 1)
            {
                Nodo nuevo = new Nodo();
                nuevo.Info = x;
                if (pos == 1)
                {
                    nuevo.Sig = raiz;
                    raiz = nuevo;
                }
                else
                    if (pos == Cantidad() + 1)
                {
                    Nodo reco = raiz;
                    while (reco.Sig != null)
                    {
                        reco = reco.Sig;
                    }
                    reco.Sig = nuevo;
                    nuevo.Sig = null;
                }
                else
                {
                    Nodo reco = raiz;
                    for (int f = 1; f <= pos - 2; f++)
                        reco = reco.Sig;
                    Nodo siguiente = reco.Sig;
                    reco.Sig = nuevo;
                    nuevo.Sig = siguiente;
                }
            }
        }

        public T Extraer(int pos)
        {
            T informacion;
            if (pos == 1)
            {
                informacion = raiz.Info;
                raiz = raiz.Sig;
            }
            else
            {
                Nodo reco;
                reco = raiz;
                for (int f = 1; f <= pos - 2; f++)
                    reco = reco.Sig;
                Nodo prox = reco.Sig;
                reco.Sig = prox.Sig;
                informacion = prox.Info;
            }
            return informacion;
        }

        public void Imprimir()
        {
            Nodo reco = raiz;
            while (reco != null)
            {
                Console.Write(reco.Info + "-");
                reco = reco.Sig;
            }
            Console.WriteLine();
        }

        public int Cantidad()
        {
            int cant = 0;
            Nodo reco = raiz;
            while (reco != null)
            {
                reco = reco.Sig;
                cant++;
            }
            return cant;
        }

    }

    class Program
    {
        static void Main(string[] args)
        {
            ListaGenerica<int> lista1 = new ListaGenerica<int>();
            lista1.Insertar(1, 10);
            lista1.Insertar(2, 50);
            lista1.Insertar(3, 70);
            lista1.Imprimir();

            ListaGenerica<string> lista2 = new ListaGenerica<string>();
            lista2.Insertar(1, "uno");
            lista2.Insertar(2, "dos");
            lista2.Insertar(3, "tres");
            lista2.Imprimir();

            ListaGenerica<Persona> lista3 = new ListaGenerica<Persona>();
            lista3.Insertar(1, new Persona { Nombre = "juan", Edad = 20 });
            lista3.Insertar(2, new Persona { Nombre = "ana", Edad = 12 });
            lista3.Insertar(3, new Persona { Nombre = "luis", Edad = 40 });
            lista3.Imprimir();

            Console.ReadKey();
        }
    }
}

Declaramos la clase indicando el tipo 'T' (podemos disponer cualquier nombre en lugar de 'T', pero por convención se suele utilizar este caracter):

    class ListaGenerica<T>

La estructura del nodo define una propiedad llamada Info de tipo 'T':

        class Nodo
        {
            public T Info { get; set; }
            public Nodo Sig { get; set; }
        }

El método Insertar recibe la posición donde se inserta y el parámetro de tipo 'T':

        public void Insertar(int pos, T x)
        {
            if (pos <= Cantidad() + 1)
            {
                Nodo nuevo = new Nodo();
                nuevo.Info = x;
                if (pos == 1)
                {
                    nuevo.Sig = raiz;
                    raiz = nuevo;
                }
                else
                    if (pos == Cantidad() + 1)
                {
                    Nodo reco = raiz;
                    while (reco.Sig != null)
                    {
                        reco = reco.Sig;
                    }
                    reco.Sig = nuevo;
                    nuevo.Sig = null;
                }
                else
                {
                    Nodo reco = raiz;
                    for (int f = 1; f <= pos - 2; f++)
                        reco = reco.Sig;
                    Nodo siguiente = reco.Sig;
                    reco.Sig = nuevo;
                    nuevo.Sig = siguiente;
                }
            }
        }

De la misma forma el método Extraer retorna un dato de tipo 'T':

        public T Extraer(int pos)
        {
            T informacion;
            if (pos == 1)
            {
                informacion = raiz.Info;
                raiz = raiz.Sig;
            }
            else
            {
                Nodo reco;
                reco = raiz;
                for (int f = 1; f <= pos - 2; f++)
                    reco = reco.Sig;
                Nodo prox = reco.Sig;
                reco.Sig = prox.Sig;
                informacion = prox.Info;
            }
            return informacion;
        }

Para crear una lista de enteros utilizamos la sintaxis:

            ListaGenerica<int> lista1 = new ListaGenerica<int>();

Luego podemos insertar un conjunto de enteros en distintas posiciones en la lista e imprimir la lista:

            lista1.Insertar(1, 10);
            lista1.Insertar(2, 50);
            lista1.Insertar(3, 70);
            lista1.Imprimir();

De la misma forma creamos ahora una lista con componentes de tipo string:

            ListaGenerica<string> lista2 = new ListaGenerica<string>();
            lista2.Insertar(1, "uno");
            lista2.Insertar(2, "dos");
            lista2.Insertar(3, "tres");
            lista2.Imprimir();

Para trabajar una lista de tipo Persona debemos por un lado declarar la clase Persona:

    class Persona
    {
        public string Nombre { get; set; }
        public int Edad { get; set; }

        public override string ToString()
        {
            return "(" + Nombre + "-" + Edad + ")";
        }
    }

Y en la Main creamos una lista de tipo Persona e insertamos objetos de dicha clase:

            ListaGenerica<Persona> lista3 = new ListaGenerica<Persona>();
            lista3.Insertar(1, new Persona { Nombre = "juan", Edad = 20 });
            lista3.Insertar(2, new Persona { Nombre = "ana", Edad = 12 });
            lista3.Insertar(3, new Persona { Nombre = "luis", Edad = 40 });
            lista3.Imprimir();

Cuando llamamos al método Imprimir de la clase ListaGenerica y se ejecuta la línea:

                Console.Write(reco.Info + "-");

Al acceder a reco.Info se llama el método ToString() de la clase Persona.

Por pantalla al ejecutar este programa tenemos como resultado:

genéricos

Utilizar genéricos en C# nos reduce mucho el código a implementar, podemos utilizar la misma clase administrando distintos tipos de datos.


Retornar