miércoles, 21 de mayo de 2014

Tipos Genéricos en C# - Parte 2: Beneficios

Tabla de Contenido

0. Introducción
1. Antes de la Versión 2.0 de .NET Framework
1.1 Carencia de comprobación de tipos en tiempo de compilación
1.2 ¿Los tipos genéricos cómo atacan estos problemas?
1.3 Una solución de tipos no-genéricos en versiones 1.0 y 1.1 del lenguaje C#
2. Tipos No-Genéricos vs Tipos Genéricos
3. Conclusiones
4. Glosario
5. Literatura & Enlaces

0. Introducción

En la primera parte de esta serie de artículos de tipos genéricos en C# vimos el concepto básico de tipo genérico: comprende una plantilla para el tratamiento generalizado de los tipos en, por ejemplo, en una colección, además del aporte de reusabilidad, seguridad de tipos, y mejor desempeño en la manipulación de ingentes cantidades de datos. Ahora, llegamos a un punto donde podemos explorar estas propiedades: beneficios de tipos genéricos.

1. Antes de la Versión 2.0 de .NET Framework

En las versiones previas de la versión 2.0 del Framework .NET, los tipos genéricos no estaban disponibles, sin embargo existían métodos ad-hoc para atacar esta carencia, los cuales no eran (no son) muy atractivos porque poseen un pobre desempeño (ver sección 4), degradan la seguridad de tipos, y no promueven la reusabilidad (lo veremos en las siguientes sub-secciones.)

A continuación, algunas líneas de código del uso de creación de una lista ArrayList [3] sin uso de genéricos:

System.Collections.ArrayList lista1 = new System.Collections.ArrayList();
lista1.Add(13);
lista1.Add(113);

System.Collections.ArrayList lista2 = new System.Collections.ArrayList();
lista2.Add("Blog xCSw");
lista3.Add("Blog AndroiDEV");

Estas líneas, hasta aquí, no presentan ningún problema (aparentemente, claro). Primero debemos saber que los elementos que agregamos a nuestras listas (i.e., lista1, y lista2) son implícitamente convertidos (upcast) a Object. Para la primera lista (lista1) hay que destacar que los valores que agregamos son numéricos, primero debe ocurrir la operación de envoltura (o boxing -cfr. Boxing y Unboxing en C#-) (esto demanda aún mas recursos: memoria y ciclos de procesador).

Por otro lado, miremos lo que ocurre al tratar de hacer algo como esto:

int numeroEntero = lista1[0];

En este caso, el compilador generará el error CS0266 [4], debido a que no es posible realizar una conversión (casting) de object a int directamente (como podría ser deseable). (Se debe realizar explícitamente. Como veremos más adelante esto representa un costo de ciclos de procesador y espacio en memoria de trabajo que en proyectos de ingeniería de software no podemos darnos el lujo de desperdiciar debido a la complejidad del problema o las restricciones impuestas por los stakeholders o la misma infraestructura IT de la organización.):

error CS0266: Cannot implicitly convert type `object' to `int'. An explicit conversion exists (are you missing a cast?)

En esta otra línea,

string cadena = lista2[0];

a pesar de que string es un tipo por referencia, el error CS0266 se genera de nuevo:

error CS0266: Cannot implicitly convert type `object' to `string'. An explicit conversion exists (are you missing a cast?)


Para solucionar estos errores, simplemente basta con realizar una conversión explícita (downcast):

int numeroEntero = (int)lista1[0];
string cadena = (string)lista2[0];

sin embargo, estas operaciones son costosas a nivel de recursos de máquina (como ya se ha mencionado arriba).

Podemos sumar que la generalización de tipos se logra a través de la conversión (casting) desde y hacia el tipo base universal -Object- (cfr. La clase Object y sus Miembros) [2].

Desde [2] podemos agregar (textualmente):
...Both the casting and the boxing and unboxing operations decrease performance; the effect of boxing and unboxing can be very significant in scenarios where you must iterate over large iterations.

1.1 Carencia de comprobación de tipos en tiempo de compilación

Miremos a través de estas líneas de código la omisión de comprobación de tipos que hace el compilador:

System.Collections.ArrayList listaEnteros = new System.Collections.ArrayList();

// Agregación de tipos enteros:
listaEnteros.Add(3);
listaEnteros.Add(11);

// Agregación de un objeto string.
// No se generá ningún tipo de error en tiempo de
// compilación, pero si posiblemente en tiempo de ejecución:
listaEnteros.Add("Blog xCSw");

int acumulador = 0;

// Itera la lista `listaEnteros`, sin embargo se produce una excepción
// (InvalidCastException):
foreach(int numero in listaEnteros)
{
acumulador += numero;
}


A pesar de que el compilador no se desgasta comprobando los tipos que agregamos a la lista, esto debido, a que todo los elementos que agregamos se convierten al tipo universal Object, esto sí que incrementa el número de excepciones y problemas que se pueden generar en tiempo de compilación. La sentencia listaEnteros.Add("Blog xCSw");, en este caso agrega un tipo de dato incompatible para la lista.


Resultado:
Figura 1. Intento de ejecución de SinComprobacionTipos.exe: InvalidCastException.

1.2 Los tipos genéricos, ¿cómo atacan estos problemas?

En la sección Historia del primer artículo de esta serie aprendimos que a partir de la versión 2.0 el Framework .NET contamos con un nuevo tipo de construcción: tipos genéricos. Estos tipos genéricos se hayan distribuidos en el nombre de espacios System.Collections.Generic [7] . Mostramos además un ejemplo de lista genérico que voy a repetir en esta sección para demostrar la forma en que los tipos genéricos entran en acción para remover el problema de comprobación de tipos (sección 1.1):


Observemos en las líneas 34, 37, y 40 la creación de listas genéricas con los parámetros de tipo: intstring, y ClaseEjemplo, respectivamente:

// Lista con tipo int:
ListaGenerica<int> listaEnteros = new ListaGenerica<int>();

// Lista con cadenas de caracteres (string):
ListaGenerica<string> listaStrings = new ListaGenerica<string>();

// Lista con tipo ClaseEjemplo:
ListaGenerica<ClaseEjemplo> listaClaseEjemplo = new ListaGenerica<ClaseEjemplo>();

Con el hecho de especificar el parámetro de tipo entre los paréntesis angulares, el compilador debe comprobar los valores que agregamos a la lista. Podemos ver a esta operación como el indicador de restricción para la agrecición de elementos a la lista.

Echemos un vistazo a lo qué ocurre cuando intentamos agregar una instancia de un tipo de dato diferente a int a la lista `listaEnteros`:

listaEnteros.Agregar("Blog xCSw");

El error en tiempo de compilación CS1503, manifiesta que no es posible convertir un tipo string a int:


error CS1503: Argument `#1' cannot convert `string' expression to type `int'

1.3 Una solución de tipos no genéricos en versiones 1.0 y 1.1 del lenguaje C#

A pesar de que presentamos la solución ideal en la sección anterior, debido a que corresponde a detectar enseguida las ventajas de reusabilidad y seguridad de tipos en los tipos genéricos. Ahora miremos cómo hubiera logrado un efecto similar (pero inviable a mediano o largo plazo) un programador de versiones 1.0 y 1.1 del lenguaje de programación C#.

Hablemos nuevamente de una lista (estructura de datos o colección, como prefieran referirse a esta) como ArrayList. El programador requerirá crear un clase por cada tipo de dato que desee manipular. Creemos tres clases para string, int, y double:

Archivo ColeccionesNoGenericas.cs:

Nada más observemos cómo aumenta el número de clases por cada tipo que vayamos a manipular en una colección lista. Evidentemente, si tuviéramos en nuestro modelo 100 clases, tendríamos que crear ese número equivalente de tipos de estructuras de datos:

ListaClase1
ListaClase2
ListaClase3
...
ListaClase100

Esto no promueve de ninguna manera la reusabilidad. Asunto que si lo logramos con los tipos genéricos como en el ejemplo de ListaGenerica.cs en la sub-sección anterior (1.2).

2. Tipos No-Génericos vs Tipos Genéricos

Vamos a crear una clase de ejemplo, y dos estructuras propuestas que junten los conceptos de las sub-secciones 1.2 y 1.3.

Con List<T> vamos a utilizar el concepto de tipos genéricos, mientras con ArrayList, utilizaremos el viejo concepto de C# 1.0 y C# 1.1.

Archivo PruebaRendimientoGenericos.cs:

Datos estadísticos obtenidos de las 10 pruebas por cada tipo en milisegundos (ms):
  • ArrayList:
    • 122.21
    • 102.99
    • 135.89
    • 424.09
    • 327.06
    • 298.62
    • 378.50
    • 359.89
    • 376.33
    • 124.73
  • List<T>:
    • 11.07
    • 8.27
    • 8.08
    • 104.49
    • 21.39
    • 63.35
    • 22.85
    • 21.33
    • 8.13
    • 10.48
Medidas estadísticas:
  • Media:
    • ArrayList:
      • 265.03
    • List<T>:
      • 27.94
  • Mínimo:
    • ArrayList:
      • 102.99
    • List<T>:
      • 8.08
  • Máximo:
    • ArrayList:
      • 424.09
    • List<T>:
      • 104.49
Resulta evidente que el desempeño de los tipos genéricos es varias veces mejor (por no decir arbitrariamente que alrededor de 10 veces (según estas pruebas)).

Resultado (evidencia):

3. Conclusiones

Hemos conocido los beneficios de los tipos genéricos frente a los tipos no-genéricos  (que resultan evidentes: ver sección 2). Realizamos una prueba de agregación de números enteros a ambos tipos de estructuras, y de forma arbitraria (según los resultados de las pruebas, por supuesto) podría decir que los tipos genéricos son ~10 más rápidos que los tipos no-genéricos, lo cual representa una oportunidad de uso en aplicaciones que requieren alto desempeño.

4. Glosario

  • Clase
  • Compilador
  • Comprobación de tipos
  • Desempeño
  • Genérico
  • Rendimiento
  • Tiempo de compilación
  • Tiempo de ejecución
  • Tipo

5. Literatura & Enlaces

[1]: C# 5.0 in a Nutshell by Joseph Albahari and Ben Albahari. Copyright 2012 Joseph Albahari and Ben Albahari, 978-1-449-32010-2.
[2]: Benefits of Generics (C# Programming Guide) - http://msdn.microsoft.com/en-us/library/b5bx6xee.aspx
[3]: ArrayList Class (System.Collections) - http://msdn.microsoft.com/en-us/library/system.collections.arraylist(v=VS.100).aspx
[4]: Compiler Error CS0266 - http://msdn.microsoft.com/en-us/library/5z8wb9e4.aspx
[5]: La Clase Object y sus Miembros en C# | OrtizOL - Experiencias Construcción Software (xCSw) - http://ortizol.blogspot.com/2014/04/la-clase-object-y-sus-miembros-en-c.html
[6] Boxing y UnBoxing en C# - http://ortizol.blogspot.com/2014/03/boxing-y-unboxing-en-c.html
[7]: System.Collections.Generic Namespace () - http://msdn.microsoft.com/en-us/library/system.collections.generic.aspx


J

No hay comentarios:

Publicar un comentario

Envíe sus comentarios, dudas, sugerencias, críticas. Gracias.