Introducción a Generics en .NET

Los Generics en .NET son una herramienta muy importante que todos los desarrolladores de C# deberían dominar, es un tema que nunca falta en las entrevistas de trabajo para algún proyecto relacionado a esta tecnología, por lo cual es importante conocer que son los Generics y cómo utilizarlos, el dominio de este concepto es uno de los cuales diferencia a un developer Middle de un Senior.

Pero bueno, ¿que son los Generics?

La definición que provee la documentación oficial es que son un concepto introducido en la versión 2.0 del .NET framework que permite parametrizar tipos (type parameters), lo cual permite diseñar clases y métodos reutilizables de forma genérica hasta que estos se declaren e instancien en el código a utilizarlos. Una definición algo confusa si no se tiene un background real de cómo funcionan, pero no se preocupen, lo explicare a continuación.

Al igual que un método puede recibir parámetros para realizar alguna operación con su valor, también se pueden parametrizar el tipo que recibirá una clase o método, estos parámetros por lo general se identifican por el prefijo T y que se encuentran entre los símbolos “<” y “>”:

class Program
{
    static void Main(string[] args)
    {
        GenericsDemo<int> demoInt 
              = new GenericsDemo<int>();

        demoInt.DoSomething(10);

        GenericsDemo<string> demoString 
              = new GenericsDemo<string>();

        demoString.DoSomething("Hello world!");            
    }
}

public class GenericsDemo<T>
{
    public void DoSomething(T parameter)
    {
        //do something with a generic type
    }
}

A partir de la clase anterior se puede aprovechar la funcionalidad de los Generics para evitar repetir código para cada uno de los diferentes tipos que la aplicación utilizara, GenericDemo indica que la clase recibirá un tipo al ser instanciada, en este caso int y string, este parámetro podrá ser después reemplazado con cualquier otro tipo como bool, char, User, Product, Car, Dog, etc…

Ejemplo

Supongamos que llega un nuevo requerimiento para una aplicación de venta de juguetes, en la cual se deben de agregar un máximo de 5 elementos por lista ya que la computadora en el cual se ejecutara el sistema tiene una memoria limitada, por lo que las listas de usuarios, productos, etc.. no pueden exceder los 5 ítems al listarlos, la forma más “simple” para resolver esto sería el crear una lista para cada uno de estos elementos agregando una validación en cada una:

public class User { }
public class Product { }

public class LimitedListOfUsers
{
    public List<User> Users { get; set; }

    public LimitedListOfUsers()
    {
        Users = new List<User>();
    }

    public void AddUser (User newUser)
    {
        if (Users.Count < 5)
        {
            Users.Add(newUser);
        }
        else
        {
            throw new IndexOutOfRangeException(
                @"No se pueden agregar 
                  mas de 5 usuarios a la lista"
                );
        }
    }
}

public class LimitedListOfProducts
{
    public List<Product> Products { get; set; }

    public LimitedListOfProducts()
    {
        Products = new List<Product>();
    }

    public void AddProduct(Product newProduct)
    {
        if (Products.Count < 5)
        {
            Products.Add(newProduct);
        }
        else
        {
            throw new IndexOutOfRangeException(
                @"No se pueden agregar 
                  mas de 5 productos a la lista"
                );
        }
    }
}

A simple vista no parece gran cosa, copiamos, pegamos, cambiamos nombres y ¡listo!, pero… ¿y si son más de 1000 elementos diferentes? ¿Y que tal el mantenimiento del código? un simple cambio en la función Add tendría que realizarse en todas esas clases, ¿que tal si se crea una clase que tenga una lista de tipo object? De esa forma se podrían crear todo tipo de listas con una sola clase como la siguiente:

public class LimitedListOfObjects
{
    public List<object> Objects { get; set; }

    public LimitedListOfObjects()
    {
        Objects = new List<object>();
    }

    public void AddObject(object newObject)
    {
        if (Objects.Count < 5)
        {
            Objects.Add(newObject);
        }
        else
        {
            throw new IndexOutOfRangeException(
                @"No se pueden agregar 
                  mas de 5 objetos a la lista"
                );
        }
    }
}

Existen dos grandes problemas con esto, uno es que al ser listas de tipo objeto, en ellas se podrán agregar diferentes tipos de elementos, mediante esta solución será posible mezclar peras con manzanas o Usuarios y Productos en la misma lista. Otro problema es que no podríamos sacar ventaja de la herencia si todos los elementos son de tipo object, abordemos primero el primer problema creando una lista limitada con elementos genéricos:

public class LimitedList<T>
{
    public List<T> Items { get; set; }

    public LimitedList()
    {
        Items = new List<T>();
    }

    public void Add(T newItem)
    {
        if (Items.Count < 5)
        {
            Items.Add(newItem);
        }
        else
        {
            throw new IndexOutOfRangeException(
                @"No se pueden agregar 
                  mas de 5 items a la lista"
                );
        }
    }
}

¡Mucho mejor!, de esta forma será posible crear listas limitadas de elementos de cualquier tipo y con una sola clase, pero, ¿qué hay del segundo problema que comentábamos?

Restricciones

Supongamos que para las listas de productos, primero se necesita validar que el producto este en buenas condiciones antes de agregarlo a la lista, eh aquí un problema, ya que no todos los elementos son productos, por lo cual se deberá crear una lista especial para productos.

Pero ya que en el contexto de la clase genérica, T no representa a una clase en especial, no será posible indicarle que ejecute cierto método a menos que se restrinja T a un subconjunto especial de tipos, es ahí cuando una clase base entra en acción para auxiliar a que todas sus clases hijas gocen del beneficio de la herencia en las clases genéricas que las utilicen, de esta forma tenemos que nuestra lista de productos quedaría de la siguiente manera:

public class Product
{
    public bool QualityValidation()
    {
        //do validation
        return true;
    }
}

public class VideoGame : Product { }
public class BoardGame : Product { }

public class LimitedListOfProducts<T> where T : Product
{
    public List<T> Items { get; set; }

    public LimitedListOfProducts()
    {
        Items = new List<T>();
    }

    public void Add(T newItem)
    {
        bool successfulValidation 
              = newItem.QualityValidation();

        if (successfulValidation == false)
        {
            throw new Exception(
                @"Algo esta mal con el producto, 
                 no es posible agregarlo a la lista"
                );
        }

        if (Items.Count < 5)
        {
            Items.Add(newItem);
        }
        else
        {
            throw new IndexOutOfRangeException(
                @"No se pueden agregar 
                  mas de 5 items a la lista"
                );
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        LimitedListOfProducts<VideoGame> listOfVideoGames 
            = new LimitedListOfProducts<VideoGame>();

        listOfVideoGames.Add(new VideoGame());

        LimitedListOfProducts<BoardGame> listOfBoardGames 
            = new LimitedListOfProducts<BoardGame>();

        listOfBoardGames.Add(new BoardGame());
    }
}

Como pueden ver, la restricción existente en la clase LimitedListOfProducts indica que T solo puede ser de tipo Producto o una subclase de esta (linea 13), si se intenta utilizar esta lista genérica de productos con cualquier otra clase aparecerá un error de compilación indicando que T no es una subclase de Product.

Existen algunas otras restricciones que se pueden utilizar con Generics, pero esta entrada no profundizara más en el tema, espero pronto escribir algo mas al respecto y aplicar todo esto a un ejemplo de patrón repositorio (Repository Pattern) al cual se le puede sacar mucho provecho usando Generics.

Bonus

Existen muchas formas de abordar el problema anterior, a continuación, les presento una forma más elegante de tener ambas listas de forma óptima:

public class Product
{
    public bool QualityValidation()
    {
        //do validation
        return true;
    }
}

public class VideoGame : Product { }
public class BoardGame : Product { }

public class LimitedList<T>
{
    public List<T> Items { get; set; }

    public LimitedList()
    {
        Items = new List<T>();
    }

    public void Add(T newItem)
    {
        bool validSize = DoSizeValidations();

        if (validSize)
        {
            bool validPreconditions 
                = DoPreconditionsValidation(newItem);

            if (validPreconditions == false)
            {
                throw new Exception(
                    @"Algo esta mal con el item, 
                      no es posible agregarlo a la lista"
                );
            }

            Items.Add(newItem);
        }
        else
        {
            throw new IndexOutOfRangeException(
                @"No se pueden agregar 
                  mas de 5 items a la lista"
                );
        }
    }

    protected virtual bool DoSizeValidations()
    {
        if (Items.Count < 5)
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    protected virtual bool DoPreconditionsValidation(
        T newItem
        )
    {
        return true;
    }
}

public class LimitedListOfProducts<T> : 
    LimitedList<T> where T : Product
{
    protected override bool DoPreconditionsValidation(
        T newItem
        )
    {
        bool successfulValidation 
            = newItem.QualityValidation();

        return successfulValidation;
    }
}

class Program
{
    static void Main(string[] args)
    {
        LimitedListOfProducts<VideoGame> listOfVideoGames 
            = new LimitedListOfProducts<VideoGame>();

        listOfVideoGames.Add(new VideoGame());

        LimitedListOfProducts<BoardGame> listOfBoardGames 
            = new LimitedListOfProducts<BoardGame>();

        listOfBoardGames.Add(new BoardGame());
    }
}

Sin embargo, esta solución viola uno de los principios fundamentales de la programación, los invito a que lo analicen y compartan sus conclusiones.

¡Saludos y gracias por su atención, no olviden compartir esta entrada!

Sígueme y comparte este sitio!

Comparte tu opinión!

comentarios

Autor: ccaldera

Entusiasta de las tecnologías de información, conferencista, ingeniero y maestro en sistemas computacionales, actualmente trabajo como .NET Tech Lead en Unosquare México y como Senior Web developer en Toptal.com, cuento con 11 años de experiencia en el ámbito de las TI’s de los cuales los últimos 7 han sido como web developer.