Genéricos
Los
tipos genéricos se agregaron a la versión 2.0 del lenguaje C# y Common Language
Runtime (CLR). Estos tipos agregan el concepto de parámetros de tipo a .NET
Framework, lo cual permite diseñar clases y métodos que aplazan la especificación
de uno o más tipos hasta que el código de cliente declara y crea una instancia
de la clase o del método. Por ejemplo, mediante la utilización de un parámetro
de tipo genérico T, se puede escribir una clase única que otro código de
cliente puede utilizar sin generar el costo o el riesgo de conversiones en
tiempo de ejecución u operaciones de conversión boxing, como se muestra a
continuación:
// Declare the generic class.
public class GenericList<T>
{
void Add(T input) { }
}
class TestGenericList
{
private class ExampleClass { }
static void Main()
{
// Declare a list of type int.
GenericList<int> list1 = new GenericList<int>();
// Declare a list of type string.
GenericList<string> list2 = new GenericList<string>();
// Declare a list of type ExampleClass.
GenericList<ExampleClass> list3 = new GenericList<ExampleClass>();
}
}
Información
general acerca de los genéricos
·
Utilice
los tipos genéricos para maximizar la reutilización, seguridad de tipos y
rendimiento del código.
·
El
uso más común de genéricos es crear clases de colección.
·
La
biblioteca de clases de .NET Framework contiene varias nuevas clases de
colección genéricas en el espacio de nombres System.Collections.Generic. Éstas se deberían utilizar siempre que sea posible
en lugar de clases como ArrayList en el espacio de nombres System.Collections.
·
Puede
crear sus propias interfaces, clases, métodos, eventos y delegados genéricos.
·
Las
clases genéricas se pueden restringir para permitir el acceso a métodos en
tipos de datos determinados.
·
Se
puede obtener información sobre los tipos que se utilizan en un tipo de datos
genérico en tiempo de ejecución y mediante reflexión.
Las
clases y los métodos genéricos combinan reusabilidad, seguridad de tipos y
eficacia de una manera que sus homólogos no genéricos no pueden. Los tipos
genéricos se utilizan frecuentemente con las colecciones y los métodos que
funcionan en ellas. La versión 2.0 de la biblioteca de clases de .NET Framework
proporciona un nuevo espacio de nombres, System.Collections.Generic, que contiene varias clases nuevas de colección
basadas en tipos genéricos. Se recomienda que todas las aplicaciones destinadas
a .NET Framework 2.0 y versiones posteriores utilicen las nuevas clases de
colección genéricas en lugar de sus homólogas no genéricas anteriores,
como ArrayList. Para obtener más información, vea Tipos
genéricos en la biblioteca de clases de .NET Framework.
Claro
está que también se pueden crear tipos y métodos genéricos personalizados para
proporcionar soluciones generalizadas propias y diseñar modelos eficaces que
tengan seguridad de tipos. El ejemplo de código siguiente muestra una clase de
lista vinculada genérica para propósitos de demostración. (En la mayoría de los
casos, es aconsejable utilizar la clase List<T> proporcionada por la biblioteca de clases de
.NET Framework en lugar de crear una propia.) El parámetro de
tipo T se emplea en diversas ubicaciones donde generalmente se
utilizaría un tipo concreto para indicar el tipo del elemento almacenado en la
lista. Este parámetro se utiliza de las formas siguientes:
·
Como
tipo de un parámetro de método en el método AddHead.
·
Como
tipo de valor devuelto del método público GetNext y la
propiedad Data en la clase anidada Node.
·
Como
tipo de datos de los miembros privados en la clase anidada.
Observe
que T está disponible para la clase anidada Node. Cuando se creen
instancias de GenericList<T> con un tipo concreto, por
ejemplo GenericList<int>, cada aparición de T se
reemplazará por int.
// type parameter T in angle brackets
public class GenericList<T>
{
// The nested class is also generic on T.
private class Node
{
// T used in non-generic constructor.
public Node(T t)
{
next = null;
data = t;
}
private Node next;
public Node Next
{
get { return next; }
set { next = value; }
}
// T as private member data type.
private T data;
// T as return type of property.
public T Data
{
get { return data; }
set { data = value; }
}
}
private Node head;
// constructor
public GenericList()
{
head = null;
}
// T as method parameter type:
public void AddHead(T t)
{
Node n = new Node(t);
n.Next = head;
head = n;
}
public IEnumerator<T> GetEnumerator()
{
Node current = head;
while (current != null)
{
yield return current.Data;
current = current.Next;
}
}
}
En el ejemplo de código
siguiente se muestra cómo el código de cliente utiliza la clase
genérica GenericList<T> para crear una lista de enteros. Con
sólo cambiar el argumento de tipo, el código siguiente puede modificarse
fácilmente para crear listas de cadenas o cualquier otro tipo personalizado:
class TestGenericList
{
static void Main()
{
// int is the type argument
GenericList<int> list = new GenericList<int>();
for (int x = 0; x < 10; x++)
{
list.AddHead(x);
}
foreach (int i in list)
{
System.Console.Write(i + " ");
}
System.Console.WriteLine("\nDone");
}
}
Ventajas de los
genéricos
Los tipos genéricos
proporcionan la solución a una limitación de las versiones anteriores de Common
Language Runtime y del lenguaje C#, en los que se realiza una generalización
mediante la conversión de tipos a y desde el tipo base universal Object. Con la creación de una clase genérica, se puede
crear una colección que garantiza la seguridad de tipos en tiempo de
compilación.
Las limitaciones del uso de
clases de colección no genéricas se pueden demostrar escribiendo un breve
programa que utilice la clase de colección ArrayList de la biblioteca de clases base de .NET
Framework. ArrayList es una clase de colección muy conveniente, que
se puede utilizar sin modificar para almacenar tipos de referencia o tipos de
valor.
// The .NET Framework 1.1 way to create a list:
System.Collections.ArrayList list1 = new System.Collections.ArrayList();
list1.Add(3);
list1.Add(105);
System.Collections.ArrayList list2 = new System.Collections.ArrayList();
list2.Add("It is raining in Redmond.");
list2.Add("It is snowing in the mountains.");
Pero esta conveniencia tiene
su costo. Cualquier referencia o tipo de valor agregado a un objeto ArrayList se convierte implícitamente a Object. Si los elementos son tipos de valor, se les debe
aplicar la conversión boxing cuando se agregan a la lista y la conversión
unboxing cuando se recuperan. Tanto las operaciones de conversión de tipos como
las de conversiones boxing y unboxing reducen el rendimiento; el efecto de las
conversiones boxing y unboxing puede ser muy notable en los casos en los que se
deben recorrer en iteración colecciones extensas.
La otra limitación es la
ausencia de comprobación de tipos en tiempo de compilación; dado que un
objeto ArrayList convierte todo a Object, en tiempo de compilación no hay forma de evitar
que el código de cliente haga cosas como la siguiente:
System.Collections.ArrayList list = new System.Collections.ArrayList();
// Add an integer to the list.
list.Add(3);
// Add a string to the list. This will compile, but may cause an error later.
list.Add("It is raining in Redmond.");
int t = 0;
// This causes an InvalidCastException to be returned.
foreach (int x in list)
{
t += x;
}
Aunque es perfectamente
válido y a veces intencionado si se crea una colección heterogénea, es probable
que la combinación de cadenas y valores ints en un objeto ArrayList único sea un error de programación, el cual no
se detectará hasta el tiempo de ejecución.
En las versiones 1.0 y 1.1
del lenguaje C#, se podían evitar los riesgos de utilizar código generalizado
en las clases de colección de la biblioteca de clases base de .NET Framework
escribiendo colecciones propias específicas del tipo. Claro está que, como
dicha clase no se puede reutilizar para más de un tipo de datos, se pierden las
ventajas de la generalización y se debe volver a escribir la clase para cada
uno de los tipos que se van a almacenar.
Lo que ArrayList y otras clases similares realmente necesitan
es un modo de que el código de cliente especifique, por instancias, el tipo de
datos particular que se va a utilizar. Eso eliminaría la necesidad de convertir
a T:System.Object y también haría posible que el compilador realizara
la comprobación de tipos. Es decir, ArrayList necesita un parámetro de tipo. Eso es
precisamente lo que los tipos genéricos proporcionan. En la colección
genérica List<T>, en el espacio de
nombres N:System.Collections.Generic, la misma operación de agregar
elementos a la colección tiene la apariencia siguiente:
// The .NET Framework 2.0 way to create a list
List<int> list1 = new List<int>();
// No boxing, no casting:
list1.Add(3);
// Compile-time error:
// list1.Add("It is raining in Redmond.");
En el código de cliente, la
única sintaxis que se agrega con List<T> en comparación con ArrayList es el argumento de tipo en la declaración y
creación de instancias. A cambio de esta complejidad de codificación
ligeramente mayor, se puede crear una lista que no sólo es más segura que ArrayList, sino que también es bastante más rápida, en
especial cuando los elementos de lista son tipos de valor.
Parámetros de
tipos genéricos
En una definición de tipo o
método genérico, un parámetro de tipo es un marcador para un tipo especificado
por un cliente al crear una instancia de una variable del tipo genérico. Una
clase genérica, como GenericList<T>, que se muestra en Introducción
a los genéricos, no se puede utilizar tal cual, porque no es realmente un tipo, sino el
plano de un tipo. Para utilizar GenericList<T>, el código de cliente
debe declarar y crear una instancia de un tipo construido especificando un
argumento de tipo dentro de los corchetes angulares. El argumento de tipo para
esta clase determinada puede ser cualquier tipo reconocido por el compilador.
Se puede crear cualquier número de instancias del tipo construido, cada una de
ellas con un argumento de tipo diferente, de la forma siguiente:
GenericList<float> list1 = new GenericList<float>();
GenericList<ExampleClass> list2 = new GenericList<ExampleClass>();
GenericList<ExampleStruct> list3 = new GenericList<ExampleStruct>();
En cada una de estas
instancias de GenericList<T>, cada aparición de T en la
clase se sustituirá en tiempo de ejecución con el argumento de tipo. Mediante
esta sustitución, hemos creado tres objetos independientes y eficaces con
seguridad de tipos utilizando una sola definición de clase. Para obtener más
información sobre cómo el CLR realiza esta sustitución, vea Genéricos
en el motor en tiempo de ejecución.
Instrucciones de
nomenclatura de parámetros de tipo
·
Denomine los parámetros de tipo genérico con nombres descriptivos,
a menos que un nombre de una sola letra sea muy fácil de entender y un nombre
descriptivo no agregue ningún valor.
C#
public interface ISessionChannel<TSession> { /*...*/ }
public delegate TOutput Converter<TInput, TOutput>(TInput from);
public class List<T> { /*...*/ }
·
Considere el
uso de T como nombre del parámetro de tipo para los tipos con un parámetro de
tipo de una sola letra.
C#
public int IComparer<T>() { return 0; }
public delegate bool Predicate<T>(T item);
public struct Nullable<T> where T : struct { /*...*/ }
·
Añada el prefijo "T" a los nombres de
parámetros de tipo descriptivos.
C#
public interface ISessionChannel<TSession>
{
TSession Session { get; }
}
·
Considere indicar
las restricciones de un parámetro de tipo en el nombre del parámetro. Por
ejemplo, un parámetro restringido a ISessionse puede denominar TSession.
Restricciones de
tipos de parámetros
Cuando se define una clase
genérica, se pueden aplicar restricciones a las clases de tipos que el código
de cliente puede usar para argumentos de tipo cuando crea una instancia de la
clase. Si el código de cliente intenta crear una instancia de la clase con un
tipo que no está permitido por una restricción, el resultado es un error de
compilación. Estas limitaciones se llaman restricciones. Las restricciones se
especifican mediante la palabra clave contextual where. En la siguiente
tabla se muestran los seis tipos de restricción:
Restricción |
Descripción |
where T: struct |
El argumento de tipo debe ser un
tipo de valor. Se puede especificar cualquier tipo de valor excepto Nullable. Para obtener más información, consulte Utilizar tipos
que aceptan valores NULL. |
where T : class |
El argumento de tipo debe ser un
tipo de referencia; esto se aplica también a cualquier tipo de clase,
interfaz, delegado o matriz. |
where T : new() |
El argumento de tipo debe tener un
constructor público sin parámetros. Cuando se utiliza la restricción new() con otras restricciones, debe
especificarse en último lugar. |
where T : <nombre de clase
base> |
El argumento de tipo debe ser la
clase base especificada, o bien debe derivarse de la misma. |
where T: <nombre de interfaz> |
El argumento de tipo debe ser o
implementar la interfaz especificada. Se pueden especificar varias
restricciones de interfaz. La interfaz con restricciones también puede ser
genérica. |
where T : U |
El argumento de tipo proporcionado
para T debe ser o derivar del argumento proporcionado para U. |
Por qué utilizar
restricciones
Si
desea examinar un elemento en una lista genérica para determinar si es válido o
compararlo con otro elemento, el compilador debe tener alguna garantía de que
el operador o método que tiene que llamar será compatible con cualquier
argumento de tipo que el código de cliente pudiera especificar. Esta garantía
se obtiene al aplicar una o más restricciones a la definición de clase
genérica. Por ejemplo, la restricción de clase base le indica al compilador que
sólo los objetos de este tipo o derivados de éste se usarán como argumentos de
tipo. Una vez que el compilador tiene esta garantía, puede permitir que se
llame a los métodos de ese tipo en la clase genérica. Las restricciones se
aplican mediante la palabra clave contextual where. En el siguiente ejemplo
de código se muestra la funcionalidad que se puede agregar a la
clase GenericList<T> (en Introducción
a los genéricos) mediante la aplicación de una restricción de clase base.
public class Employee
{
private string name;
private int id;
public Employee(string s, int i)
{
name = s;
id = i;
}
public string Name
{
get { return name; }
set { name = value; }
}
public int ID
{
get { return id; }
set { id = value; }
}
}
public class GenericList<T> where T : Employee
{
private class Node
{
private Node next;
private T data;
public Node(T t)
{
next = null;
data = t;
}
public Node Next
{
get { return next; }
set { next = value; }
}
public T Data
{
get { return data; }
set { data = value; }
}
}
private Node head;
public GenericList() //constructor
{
head = null;
}
public void AddHead(T t)
{
Node n = new Node(t);
n.Next = head;
head = n;
}
public IEnumerator<T> GetEnumerator()
{
Node current = head;
while (current != null)
{
yield return current.Data;
current = current.Next;
}
}
public T FindFirstOccurrence(string s)
{
Node current = head;
T t = null;
while (current != null)
{
//The constraint enables access to the Name property.
if (current.Data.Name == s)
{
t = current.Data;
break;
}
else
{
current = current.Next;
}
}
return t;
}
}
En un aplicativo de consola, vamos a escribir dos clases DataMember y ParClaveValor
Posteriormente añadiremos otra clase denominandola DataStore
Instanciaremos luego la clase DataStore pasando los valores en los metodos AddOrUpdate: