Dar el salto a los generics de .Net 2.0 supone un cambio en la forma de pensar. Como dicen en inglés, un “mind shift”. En este artículo voy a tratar de explicar los generics mediante reglas que hagan fácil el proceso de detectar cúando se pueden usar y de qué manera. Si bien es cierto que generics no es ya nada nuevo, aún hay mucha gente que sigue con .Net 1.1. Ahora que están trabajando en .Net 4.0, es buen momento para irse actualizando.

Me voy a basar en el código fuente de mi propio frameworks DesktopRails y en ejemplos del framework CSLA.

  • ¿Qué son los Generics?

Generics es algo así como el equivalente en .Net a las templates de C++, con unas cuantas diferencias. En realidad son más potentes puesto que el compilador de C# es más avanzado. Cuando vemos un código que usa los símbolos “<>” como el siguiente, sabemos que se trata de generics:

  List numbers = new List();
  • Generics para colecciones

El uso más claro de generics es en colecciones de elementos. Con .Net 1.1 usábamos la clase ArrayList para disponer de arrays de elementos de cualquier tipo, puesto que ArrayList permite introducir objetos de tipo System.Object y en .Net todos los objetos heredan de esa clase. Entonces si necesitamos una lista de cadenas haríamos lo siguiente:

ArrayList myList = new ArrayList();
myList.Add("this is a test");
string firstItem = (string)myList[0];

En éste código vemos cómo se introducen elementos en el vector y cómo se extraen. Internamente el runtime toma el elemento como un System.Object aunque es un string, haciendo una conversion que llaman “boxing”, empaquetado. A la hora de extraer el elemento y poderlo manipular, el runtime hace “unboxing” cuando se lo indicamos con la versión de tipo (typecast). Esto tiene dos desventajas:

  1. La primera es el rendimiento. Se ha demostrado que la eficiencia es muchisimo mayor con generics porque como veremos a continuación, evitan el boxing-unboxing.
  2. La segunda es que podemos cometer errores que sólo se descubrirán en tiempo de ejecución. Si el lugar donde el elemento se introduce en el vector está lejos del lugar donde se extrae, el programador podría intentar hacer el typecast a un tipo que no es el correcto. El compilador no puede detectar ese error y es en tiempo de ejecución cuando se produciría la excepción.

Con generics existen clases en System.Collections.Generic que sirven para el mismo propósito. Esta sería la alternativa al código anterior:

List myList = new List();
myList.Add("this is a test");
string firstItem = myList[0];

Si en lugar de declarar firstItem como string hubiesemos puesto int, el compilador hubiese dado un error. Además, VisualStudio y MonoDevelop (IDEs) son capaces de usar autocompletado  o intellisense para recordarnos que el tipo es string, con lo cual es posible que nisiquiera lleguemos a cometer el error.

Forma de detectar el uso de generics: Cada vez que tenemos una colección y estamos haciendo un typecast, podemos darnos cuenta de que se puede reemplazar ese código por una colección generica.

  • Generics para relaciones fuertemente tipadas

Ahora vamos a ver usos más complejos de generics, aunque en realidad cuando se adquiere esta forma de pensar, es fácil de escribir las clases genericas. Usemos como ejemplo una clase Presenter, que se encarga de pintar vistas en la pantalla. Las vistas son de tipo IMyView. La relación entre Presenter y las instancias de IMyView es clara. Si no tuviésemos generics el código sería algo como esto:

public interface IMyView
{
     string BackgroundColor {get; set;}
}

public class SomeView : IMyView
{
	private string _bgcolor = String.Empty;

	public string BackgroundColor
	{
		get
		{
			return _bgcolor;
		}
		set
		{
			_bgcolor = value;
		}
	}
}

public class Presenter
{
    private IMyView _view;

    public IMyView View
	{
    	get
		{
    		return _view;
    	}
    }

	public Presenter(IMyView view)
	{
	   _view = view;
	}
	// business logic;
}

La forma en que usamos las clases es esta:

SomeView view = new SomeView();
Presenter presenter = new Presenter(view);
presenter.View.BackgroundColor = "black";

Si en un momento determinado vamos a usar una clase que implementa IMyView pero que ademas añade algunas propiedades o funciones que no están en la interfaz, entonces necesitamos hacer typecast:

SpecialView view = (SpecialView) presenter.View;
view.SpecialField = "whatever";

Puesto que presenter.View es del tipo IMyView, no podemos hilar más fino que eso, y desde que la jerarquía de clases crece empezamos a tener que hacer typecasts. La desventaja es la misma que en el caso anterior, que el error sólo aparece en tiempo de ejecución. La transformación a generics es sencilla:

public class GPresenter
	where T: IMyView
{
	private T _view;

	public T View
	{
	    get
		{
			return _view;
		}
	}

	public GPresenter(T view)
	{
		_view = view;
	}
    // business logic
}

SomeView smView = new SomeView();
GPresenter gpresenter = new GPresenter(smView);
gpresenter.View.BackgroundColor = "black";

Si en lugar de instanciar SomeView, instanciamos SpecialView, el autocompletado del IDE nos ofrecerá el campo SpecialField al teclear gpresenter.View.. Esto evita el error en tiempo de ejecución, lo cual es muy valioso. La clase GPresenter hace lo mismo que Presenter, pero su relación con las clases que implementan IMyView es fuértemente tipada, y eso le hace ganar en eficiencia y detección de errores.
Forma de detectar el uso de generics: Estamos haciendo typecasts desde interfaces a clases concretas.

  • Métodos con Generics

Esto no es más que una extensión de lo anterior. Supongamos que la clase GPresenter tiene un método que realiza determinada operación y como resultado devuelve un objeto. Podemos saber que el objeto será de tipo IMyView, y escribir la firma del método así:

public IMyView SomeOperation()

Sin embargo, esto nos lleva denuevo al caso del typecast anterior. La solucion es la forma generica:

public T SomeOperation()

Si este tipo es distinto del que usamos para View, podemos agregar otro tipo genérico a la definición de la clase:

public class GPresenter
   where T: IMyView
   where Y: IMyOtherInterface

Más que llamar a los parámetros genéricos, T e Y, conviene ponerles nombre, e.j, View y LoQueSea. La sintaxis de genérics no pone restricciones a los nombres que queramos poner a los parámetros.

  • Generics para envolturas (wrappers)

¿Cómo hacer genéricas clases cuyo código fuente no tenemos?. Como ejemplo os propongo el código fuente del GenericComboBox para WPF de DesktopRails. El control ComboBox de WPF tiene un campo Items que son los elementos que contiene el combo (dropdown o desplegable, como le querais llamar). Este campo es una lista de System.Object para que se pueda meter cualquier tipo de objeto y para que un combo pueda contener diferentes tipos de objeto. Sin embargo yo necesitaba saber que en determinadas ocasiones los elementos del combo eran todos del tipo string, o del tipo int para evitar errores en ejecución. La solución es crear una nueva clase que envuelve a la que nos interesa. Dentro de dicha clase estamos obligados a hacer typecast en algún momento, con lo que aquí no ganamos en eficiencia, pero ganamos en detección de errores en tiempo de compilación.

public class MyWrapper
{
    private TheClassWeWantToWrap _myWrap;
	// ...
	public List Items
	{
	    get
	{
		return (T)_myWrap.Items;
	}
	set
	{
		_myWrap.Items = value;
	}
	}
}
  • Los Generics NO son polimórficos

Una cosa que hay que tener clara es que cuando se usa un tipo genérico, no se está heredando de él, simplemente se está diciendo al compilador que haga una seria de sustituciones en la plantilla. Dada la clase MyGenericBase:

public class MyGenericBase

Las instancias:

MyGenericBase instanceY = ...;
MyGenericBase instanceX = ...;

No son hijas de MyGenericBase, solo son sustituciones. Para que haya polimorfismo hay que extender al definiar la clase:

public class PresenterChild : GPresenter
	where T: IMyView

Por tanto en diseños de clases, normalmente la clase genérica base suele implementar una interfaz de modo que tanto su jerarquía como las instancias que usen esa plantilla pertenezcan a la familia de la interfaz:

public class MyGenericBase : IMyHierarchy

Véase como ejemplo más complejo el AbstractController de DesktopRails.

  • La declaración de los tipos genéricos NO es recursiva pero sí compleja

Algunas definiciones de clases genéricas son difíciles de leer, como le ocurre a BusinessBase del framework CSLA:

public abstract class BusinessBase : BusinessBase
	where T: BusinessBase

A primera vista parece una definición recursiva, imposible de interpretar. Sin embargo el compilador sólo hace la sustitución una vez y se usaría así:

public class Customer: BusinessBase

BusinessBase es una template genérica que opera sobre un parámetro T. Así algunos métodos devuelven como resultado un objeto de tipo T. Al imponer en la cláusula “where” que el tipo T, debe ser sí mismo, lo que hacemos es eliminar el parámetro genérico en la clase que extiende de ella. Es decir, si B hereda de A, entonces B tambien es A, de ahí el truco. Esta es una definición rebuscada. El autor símplemente quería tener una clase sin parámetros genericos para que al usarla, en lugar de hacer:

BusinessBase customer = new BusinessBase();

Pudiera directamente hacer:

Customer customer = new Customer();

Pero reusando toda la jerarquia de BussinesBase que casualmente usa parámetros genéricos. Esto es un truco que podemos recordar cuando queramos eliminar los parámetros genéricos de una clase que hereda de otra genérica.

Véamos otra clase que escribí para DesktopRails. Se trata de una relación como en el primer ejemplo de este artículo, en este caso entre Controller y View. Lo que pasa es que la relación es doble. Necesitaba que desde Controller pudiera haber una referencia a View, pero desde View quería que también hubiese una relación a Controller, porque se hacen llamadas en ambos sentidos.

public interface IView : IView
	where C : AbstractController
...
public abstract class AbstractController : AbstractController
	where C : AbstractController
	where V : IView

Modo de uso:

public class UsersListController : AbstractController
...
public interface IUsersListView : IView

En este caso, una instancia de UsersListController no conoce con precision de que tipo es su View(porque la clase tiene un campo View del tipo genérico IUsersListView), solo sabe que es de la jerarquia IUsersListView. Sin embargo, la instancia de una clase que implemente IUsersListView sí que sabe con precisión cual es el tipo exacto de Controller que tiene asociado, es una relación fuertemente tipada en esta dirección.
Lo que estas cláusulas “where” aseguran es que cuando al controlador X, se le pase una vista Y, esa vista Y debe haber sido definida como vista que puede usar un controlador X.
Así podemos descubrir errores como éste en tiempo de compilación:

public interface IUsersListView: IView
...
public class UsersListController : AbstractController

Lo anterior no compilaría. Es útil darse cuenta de que la asociación que se está haciendo es incorrecta, en tiempo de compilación y no que falle en tiempo de ejecución.
Regla fácil:
Cuando vemos una declaración que parece recursiva, donde la cláusula “where” del parámetro genérico es igual a la propia declaración, eso es como decir, sí mismo. La clase está diciendo, yo misma. Entonces el modo de uso es repitiendo el nombre:

public class UsersListController : AbstractController<IUsersListView, UsersListController>

A la hora de escribir una declaración como AbstractController, sabremos que debemos usar esta sintaxis de aspecto recursivo cuando queremos hacer referencia a la propia clase que estamos definiendo. Esto será cuando queramos hacer una referencia entre varios tipos genéricos y a uno de ellos se le dice que su relación con otro es el otro mismo.

Otro ejemplo menos complicado contenido en DesktopRails es el AbstractData:

public abstract class AbstractData : IUIData

	where TBaseView : IView

	where C : AbstractController

	where TFinalView : TBaseView

En esta definición, se pasa el parámetro TBaseView sólo para forzar que es padre de TFinalView en la jerarquía. En verdad no se llega a usar TBaseView sino que sólo se usa para forzar una jerarquía. El sentido a esta relación se le ve cuando vemos clases derivadas:

public abstract class AbstractUsersListData : AbstractData, IUsersListData
		where T : IUsersListView
...
public class TestUsersListData : AbstractUsersListData

Al ver ésta última clase de la jerarquía puedes entender que estamos forzando a que TestUsersListView sea una vista que implementa IUsersListView.

La verdad es que cuando estas diseñando las clases, inicialmente a uno no se le ocurre escribir estas cláusulas a no se que tengas muuucha experiencia escribiendo clases iguales. Lo que suelo hacer, al menos yo que no tengo más materia gris que la media, es ir modificando las cláusulas según me va haciendo falta e ir dejando que el compilador me ayude a ver si todo va bien. Suelo pensar en términos de… “si alguien usa mi clase, quiero que la use de esta manera, y quiero que si la usa de tal otra forma el compilador le de un error y no le deje”. Más bién diseño al revés. Pienso en cómo quiero usar la clase y la voy diseñando. Al estilo TDD.
Existen más cláusulas “where” que se pueden especificar y que podeis leer en documentación. Por ejemplo si no sabes con exactitud la interfaz que quieres que implemente el tipo T pero sabes que quieres que sea una clase y no un tipo básico, puedes poner esto:

where T : class

Agradezco que las dudas o sugerencias sobre éste artículo se gestionen en forma de comentarios en el mismo, así como los errores que podais encontrar en él. Espero que os sea de utilidad.