Genéricos

Concepto de genéricos

    C# 2.0 permite especificar los tipos utilizados en las definiciones de otros tipos de datos y de métodos de forma parametrizada, de manera que en vez de indicarse exactamente cuáles son se coloque en su lugar un parámetro –parámetro tipo- que se concretará en el momento en que se vayan a usar (al crear un objeto de la clase, llamar al método,…) A estas definiciones se les llama genéricos, y un ejemplo de una de ellas es el siguiente:


 public class A<T>
 {
  T valor;
  public void EstablecerValor(T valor)
  {
   this.valor = valor;
  }
 }

    En esta clase no se han concretando ni el tipo del campo privado valor ni el del único parámetro del método EstablecerValor() En su lugar se le especificado un parámetro tipo T que se concretará al utilizar la clase. Por ejemplo, al crear un objeto suyo:


A<int> obj = new A<int>();

    Esto crearía un objeto de la clase genérica A con el parámetro tipo T concretizado con el argumento tipo int. La primera vez que el CLR encuentre esta concretización de T a int realizará un proceso de expansión o instanciación del genérico consistente en generar una nueva clase con el resultado de sustituir en la definición genérica toda aparición de los parámetros tipos por los argumentos tipo. Para el ejemplo anterior esta clase sería:


 public class A<int>
 {
  int valor;
  public void EstablecerValor(int valor)
  {
   this.valor = valor;
  }
 }

     A los tipos con parámetros tipo, como A<T>, se les llama tipos genéricos cerrados; a los generados al concretárseles algún parámetro tipo se le llama tipos construidos; y a los generados al concretárseles todos tipos genéricos abiertos. La relación establecida entre ellos es similar a la establecida entre las clases normales y los objetos: al igual que clases sirven de plantillas en base a las que crear objetos, los tipos genéricos cerrados actúan como plantillas en base a las que crear tipos genéricos abiertos. Por eso, en el C++ tradicional se llamaba plantillas a las construcciones equivalentes a los genéricos.

    La expansión la hace el CLR en tiempo de ejecución, a diferencia de lo que sucede en otros entornos (pe, C++) en los que se realiza al compilar. Esto tiene varias ventajas:

  • Ensamblados más pequeños: Como sólo almacenan el tipo genérico cerrado, que el CLR ya expandirá en tiempo de ejecución, su tamaño es más pequeño y se evita el problema del excesivo inflado del código binario generado (code bloat)

Además, para evitar el inflado de la memoria consumida, el CLR reutiliza gran parte del MSIL generado para la primera expansión de un genérico por un  tipo referencia en las siguientes expansiones del mismo por otros tipos referencia, ya que todas las referencias son al fin y al cabo punteros que en memoria se representan igual.

  • Metadatos ricos: Al almacenarse los tipos genéricos cerrados en los ensamblados, se podrán consultar mediante reflexión y ser aprovechados por herramientas como el IntelliSense de Visual Studio.NET para proporcionar ayuda sobre su estructura.

  • Implementación fácil: Como es el propio CLR quien realiza gran parte del trabajo necesario para dar soporte a los genéricos, la inclusión de los mismos en cualquiera de los lenguajes .NET se simplifica considerablemente.

Usos de los genéricos

    Los genéricos no son una novedad introducida por C# en el mundo de la programación, sino que otros lenguajes como Ada, Eiffel o C++ (plantillas) ya las incluyen desde hace tiempo. Su principal utilidad es, como su propio nombre indica, facilitar la creación de código genérico que pueda trabajar con datos de cualquier tipo. Esto es especialmente útil para crear tipos que actúen como colecciones (pilas, colas, listas, etc.), cosa que C# 1.X sólo permitía crear definiéndolos en base a la clase base común object. Por ejemplo, una cola que admitiese objetos de cualquier tipo había que declararla como sigue:


 public class Cola
 {
  object[] elementos;
  public int NúmeroElementos;
  
  public void Encolar(object valor);
  {…}
  public object Desencolar()
  {…}                 
 }

    El primer problema de esta solución es lo incómoda y proclive a errores que resulta su utilización, pues a la hora de extraer valores de la cola habrá que convertirlos a su tipo real si se quieren aprovechar sus miembros específicos. Es decir:


 Cola miCola = new Cola();
 miCola.Encolar("Esto es una prueba");
 string valorDesencolado = (string) miCola.Desencolar();

    Aparte de que el programador tenga que escribir (string) cada vez que quiera convertir alguna de las cadenas extraídas de miCola a su tipo concreto, ¿qué ocurrirá si por error introduce un valor que no es ni string ni de un tipo convertible a string (por ejemplo, un int) y al extraerlo sigue solicitando su conversión a string? Pues que el compilador no se dará cuenta de nada y en tiempo de ejecución saltará una InvalidCastException.

    Para resolver esto podría pensarse en derivar un tipo ColaString de Cola cuyos métodos públicos trabajasen directamente con cadenas de textos (Encolar(string valor) y string Desencolar()) Sin embargo, no es una solución fácil de reutilizar ya que para cualquier otro tipo de elementos (pe, una cola de ints) habría que derivar una nueva clase de Cola.

    Otro problema de ambas soluciones es su bajo rendimiento, puesto que cada vez que se almacene un objeto de un tipo referencia en la cola habrá que convertir su referencia a una referencia a object y al extraerlo habrá que volverla a transformar en una referencia a string. ¡Y para los tipos valor todavía es peor!, en tanto que habrá que realizar boxing y unboxing, procesos que son mucho más lentos que las conversiones de referencias.

    Si por el contrario se hubiese definido la cola utilizando genéricos tal y como sigue:


 public class Cola<T>
 {
  T[] elementos;
  public int NúmeroElementos;
  public void Encolar(T valor)
  {…}
  public T Desencolar()
  {…}                 
 }

    Entonces la extracción de objetos de la cola no requeriría de ningún tipo de conversión y sería tan cómoda y clara como sigue:


 Cola<string> miCola = new Cola<string>();
 miCola.Encolar("Esto es una prueba");
 string valorDesencolado = miCola.Desencolar();

    Si ahora por equivocación el programador solicitase almacenar un objeto cuyo tipo no fuese ni string ni convertible a él, obtendría un error al compilar informándole de ello y evitando que el fallo pueda llegar al entorno de ejecución. Además, el rendimiento del código es muy superior ya que no requerirá conversiones de referencias a/desde object. Si realiza pruebas podrá comprobar que la utilización de genéricos ofrece mejoras en el rendimiento entorno al 20% para los tipos referencia, ¡y al 200% para los tipos valor!

Sintaxis

El CLR de .NET 2.0 permite definir genéricamente tanto clases como estructuras, interfaces, delegados y métodos. Para ello basta con indicar tras el identificador de las mismas su lista de sus parámetros genéricos entre símbolos < y > separados por comas. Con ello, dentro de su definición (miembros de las clases, cuerpos de los métodos, etc.) se podrá usar libremente esos parámetros en cualquier sitio en que se espere un nombre de un tipo. La siguiente tabla muestra un ejemplo para cada tipo de construcción válida:

Ejemplo declaración

Ejemplo uso

 
public class Nodo<T>
{
public T Dato;
public Nodo<T> Siguiente;
}

class Nodo8BitAvanzado: Nodo<byte>

{...}

 

public struct Pareja<T,U>

{

public T Valor1;

public U Valor2;

}

 

Pareja<int, string> miPareja;

miPareja.Valor1 = 1;

miPareja.Valor2 = "Hola";

 

interface IComparable<T>

{

  int CompararCon(T otroValor);

}

 

class A: IComparable<Persona>

{

public int
CompararCon(Persona persona)

{...}

}

 

delegate void Tratar<T>(T valor);

  

Tratar<int> objTratar =
new Tratar<int>(F);

public void F(int x)

{...}
 

void intercambiar<T>(ref T valor1,

ref T valor2)

{

   T temp = valor1;

   valor1 = valor2;

   valor2 = temp;

}

 

decimal d1 = 0, d2 = 1;

this.intercambiar<decimal>
(ref d1, ref d2);

this.intercambiar(ref d1, ref d2);

Tabla 19: Ejemplos de declaración de tipos y miembros genéricos

    Nótese que todos los ejemplos de nombres de parámetros genéricos hasta ahora vistos son única letra mayúsculas (T, U, etc.) Aunque obviamente no es obligatorio, sino que se les puede dar cualquier identificador válido, es el convenio de nomenclatura utilizado en la BCL del .NET Framework 2.0 y el que por tanto se recomienda seguir. Lo que sí es obligatorio es no darles nunca un nombre que coincida con el del tipo o miembro al que estén asociados o con el de alguno de los miembros de éste.

    Fíjese además que la segunda llamada del ejemplo de utilización del método genérico intercambiar() no explicita el tipo del parámetro genérico. Esto se debe a que C# puede realizar inferencia de tipos y deducir que, como para todos parámetros del tipo T se ha especificado un valor decimal, T debe concretarse como decimal.

Limitaciones

    En principio, dentro de los tipos genéricos se puede declarar cualquier miembro que se pueda declarar dentro de un tipo normal, aunque existe una limitación: no se pueden declarar puntos de entrada (métodos Main())ya que a éstos los llama el CLR al iniciar la ejecución del programa y no habría posibilidad de concretizarles los argumentos tipo.

    Por su parte, los parámetros tipo se pueden usar en cualquier sitio en que se espere un tipo aunque también con ciertas limitaciones: No pueden usarse en atributos, alias, punteros o métodos externos, ni en los nombres de las clases bases o de las interfaces implementadas. Sin embargo, excepto en el caso de los punteros, en estos sitios sí que se pueden especificar tipos genéricos cerrados; e incluso en los nombres de las clases o de las interfaces base, también se pueden usar tipos genéricos abiertos. Por ejemplo:


 // class A<T>: T {} // Error. No se puede usar T como interfaz
// o clase base

 class A<T> {}
 interface I1<V> {}
 class B<T>: A<T>, I1<string> {} // OK.

    Debe tenerse cuidado al definir miembros con parámetros tipos ya que ello puede dar lugar a sutiles ambigüedades tal y como la que se tendría en el siguiente ejemplo:


 class A<T>
 {
  void F(int x, string s) {}
  void F(T x, string s) {}
 }

    Si se solicitase la expansión A<int>, el tipo construido resultante acabaría teniendo dos métodos de idéntica signatura. Aunque en principio, el compilador de C# 2.0 debería de asegurar que tras cualquier expansión siempre se generen tipos genéricos abiertos válidos, produciendo errores de compilación en caso contrario, por el momento no lo hace y deja compilar clases como la anterior. Sencillamente, si llamamos al método F() de un objeto A<int>, la versión del método que se ejecutará es la primera. Es decir, en las sobrecargas los parámetros tipo tienen menos prioridad que los concretos.

    Donde no resulta conveniente controlar que los parámetros genéricos puedan dar lugar a métodos no válidos es en las redefiniciones de operadores de conversión, en concreto en lo referente al control de que las expansiones puedan dar lugar a redefiniciones de las conversiones predefinidas (como las de a/desde object), puesto que si se hiciese los parámetros tipo nunca se podrían utilizar en las conversiones. Por ello, simplemente se ignoran las conversiones a medida que al expandirse puedan entrar en conflicto con las predefinidas. Por ejemplo, dado el siguiente código:


 using System;
 public class ConversionGenerica<T>
 {
  public static implicit operator T(ConversionGenerica<T> objeto)
  {
   T obj = default(T);
   Console.WriteLine("Operador de conversión implícita");
   return obj;                                
  }
 }
 public class Principal
 {
  public static void Main()
  {
   ConversionGenerica<Principal> objeto1=
new ConversionGenerica<Principal>();
   Principal objeto2 = objeto1; // Conversión no predefinida (a Principal)
   Console.WriteLine("Antes de conversión no predefinida");
   ConversionGenerica<object> objeto3 = new ConversionGenerica<object>();
   object objeto4 = objeto3; // Conversión predefinida (a object)
  }
 }

    El resultado de su ejecución demuestra que la versión implícita de la conversión solo se ejecuta ante la conversión no predefinida, puesto que su salida es la siguiente:

 Operador de conversión implícita

 Antes de conversión no predefinida

    Donde nunca habrá ambigüedades es ante clases como la siguiente, ya que sea cual sea el tipo por el que se concretice T siempre será uno y por tanto nunca se podrá dar el caso de que la segunda versión de F() coincida en signatura con la primera:


 class A<T>
 {
  void F(int x, string s) {}
  void F(T x, T s) {}
 }

    Así mismo, también se admitirían dos métodos como los siguientes, ya que se considera que los parámetros tipo forman parte de las signaturas:


 class A
 {
  void F<T>(int x, string s) {}
  void F(int x, string s) {}
 }

    Y al redefinir métodos habrá que mantener el mismo número de parámetros tipo en la clase hija que en la clase padre para así conservar la signatura. Esto, como en el caso de los parámetros normales, no implica mantener los nombres de los parámetros tipo, sino sólo su número. Por tanto, códigos como el siguiente serán perfectamente válidos:


 public class A
 {
  protected virtual void F<T, U>(T parámetro1, U parámetro2)
  {}
 }
 public class B:A
 {
  protected override void F<X, Y>(X parámetro1, Y parámetro2)
  {}
 }

Restricciones

    Probablemente a estas alturas ya esté pensado que si en tiempo de diseño no conoce el tipo concreto de los parámetros genéricos, ¿cómo podrá escribir código que los use y funcione independientemente de su concretización? Es decir, dado un método como:


 T Opera<T>(T valor1, T valor2)
 {
  int resultadoComparación = valor1.CompareTo(valor2);
  if (resultadoComparación>0)
     return valor1-valor2 ;
  else if(resultadoComparación<0)
     return Math.Pow(valor1, valor2) ;
  else
     return 2.0d;
    }

    Si se le llamase con Opera(2.0d, 3.2d) no habría problema, pues los objetos double tanto cuentan con un método CompareTo() válido, como tienen definida la operación de resta, se admiten como parámetros del método Pow() de la clase Math y van a generar valores de retorno de su mismo tipo double. Pero, ¿y si se le llamase con Opera("hola", "adiós")? En ese caso, aunque los objetos string también cuentan con un método CompareTo() válido, no admiten la operación de resta, no se puede pasar como parámetros a Pow() y el valor que devolvería return 2.0d; no coincidiría con el tipo de retorno del método.

    Esta inconsistencias son fáciles de solucionar en entornos donde la expansión se realiza en tiempo de compilación, pues el compilador informa de ellas. Sin embargo, en los que  se hace en tiempo de ejecución serían más graves ya que durante la compilación pasarían desapercibidas y en tiempo de ejecución podrían causar errores difíciles de detectar. Por ello, C# en principio sólo permite que con los objetos de tipos genéricos se realicen las operaciones genéricas a cualquier tipo: las de object. Por ejemplo, el código que sigue sería perfectamente válido ya que tan sólo utiliza miembros de object:


 public static bool RepresentacionesEnCadenaIguales<T,U>
(T objeto1, U objeto2)
 {
  return objeto1.ToString() == objeto2.ToString();
 }

    Obviamente, también compilará un código en el que los parámetros genéricos con los que se realicen operaciones no comunes a todos los objects se conviertan antes a tipos concretos que sí las admitan, aunque entonces si se le pasasen argumentos de tipos no válidos para esa conversión saltaría una InvalidCastException en tiempo de ejecución. Por ejemplo, el siguiente método compilará pero la ejecución fallará en tiempo de ejecución cuando se le pasen parámetros no convertibles a int.


 public static int Suma<T,U>(T valor1, U valor2)
 {
  return ((int) (object) valor1) + ((int) (object) valor2);
 }

    Nótese que por seguridad ni siquiera se permite la conversión directa de un parámetro tipo a cualquier otro tipo que no sea object y ha sido necesario hacerla indirectamente.

    Lógicamente, C# ofrece mecanismos con los que crear códigos genéricos que a la vez sean seguros y flexibles para realizar otras operaciones aparte de las que válidas para cualquier object. Estos mecanismos consisten en definir restricciones en el abanico de argumentos tipo válidos para cada parámetro tipo, de modo que así se puedan realizar con él las operaciones válidas para cualquier objeto de dicho subconjunto de tipos.

    Las restricciones se especifican con cláusulas where <parámetroGenérico>:<restricciones> tras la lista de parámetros de los métodos y delegados genéricos, o tras el identificador de las clases, estructuras e interfaces genéricas; donde <restricciones> pueden ser:

  • Restricciones de clase base: Indican que los tipos asignados al parámetro genérico deben derivar, ya sea directa o indirectamente, del indicado en <restricciones>. Así, en el código genérico se podrán realizar con seguridad todas las operaciones válidas para los objetos dicho tipo padre, incluidas cosas como lanzarlos con sentencias throw o capturarlos en bloques catch si ese padre deriva de Exception. Por ejemplo:


 using System;
 class A
 {
  public int Valor;
 }
 class B:A
 {
  public static int IncrementarValor<T>(T objeto) where T:A
  {
     return ++objeto.Valor;
  }
  static void Main() 
  {
     Console.WriteLine(B.IncrementarValor(new B()));  // Imprime 1
     Console.WriteLine(B.IncrementarValor(new A()));  // Imprime 1
     // Console.WriteLine(B.IncrementarValor(new C()));  // Error
  }
 }
 class C {}

Esta restricción además permite asegurar que el argumento tipo siempre será un tipo referencia, por lo que con podrá utilizar en los contextos en que sólo sean válidos tipos referencia, como por ejemplo con el operador as. Así mismo, también permite realizar conversiones directas del parámetro tipo a su tipo base sin tenerse que pasar antes por la conversión a object antes vista. 

  • Restricciones de interfaz: Son similares a las anteriores, sólo que en este caso lo que indican es que los tipos que se asignen al parámetro tipo deben implementar las interfaces que, separadas mediante comas, se especifiquen en <restricciones> Así se podrán usar en todos aquellos contextos en los que se esperen objetos que las implementen, como por ejemplo en sentencias using si implementan IDisposable.
  • Restricciones de constructor: Indican que los tipos por los que se sustituya el parámetro genérico deberán disponer de un constructor público sin parámetros. Se declaran especificando new() en <restricciones>, y sin ellas no se permite instanciar objetos del parámetro tipo dentro de la definición genérico. Es decir:


 // Correcto, pues se indica que el tipo por el que se concretice T
// deberá de tener un constructor sin parámetros
 class A<T> where T:new()
 {
  public T CrearObjeto()
  {
   return new T();
  }
 }
 /* Incorrecto, ya que ante el new T() no se sabe si el tipo por el 
que se concretice T tendrá un constructor sin parámetros
 class B<T>
 {
   public T CrearObjeto()
   {
     return new T();
   }
 }
*/

Cada genérico puede tener sólo una cláusula where por parámetro genérico, aunque en ella se pueden mezclar restricciones de los tres tipos separadas por comas. Si se hace, éstas han de aparecer en el orden en que se han citado: la de clase base primero y la de constructor la última. Por ejemplo, el tipo por el que se sustituya el parámetro genérico del siguiente método deberá derivar de la clase A, implementar las interfaces I1 e I2 y tener constructor público sin parámetros:


 void MiMétodo<T> (T objeto) where T: A, I1, I2, new()
 {...}

    Las restricciones no se heredan, por lo que si de una clase genérica con restricciones se deriva otra clase genérica cuyos parámetros tipo se usen como parámetros de la clase padre, también habrán de incluirse en ella dichas restricciones para asegurase de que cualquier argumento tipo que se le pase sea válido. Igualmente, en las redefiniciones habrá que mantener las restricciones específicas de cada método. O sea:


 class A {}
 class B<T> where T:A
 {}
 /* No válido, T debe ser de tipo A para poder ser pasado como argumento
tipo de B, y dicha restricción no la hereda automáticamente el T de C.
 
class C<T>:B<T>
 {}
*/
 class C<T>:B<T> where T:A // Válido
 {}

    Nótese que todas estas restricciones se basan en asegurar que los argumentos tipo tengan determinadas características (un constructor sin parámetros o ciertos miembros heredados o implementados), pero no en las propias características de esos tipos. Por lo tanto, a través de parámetros tipo no podrá llamarse a miembros estáticos ya que no hay forma de restringir los miembros estáticos que podrán tener los argumentos tipo.

    Finalmente, cabe señalar que en realidad también existe una cuarta forma de restringir el abanico de argumentos tipos de un tipo genérico, que además es mucho más flexible que las anteriores y permite aplicar cualquier lógica para la comprobación de los tipos. Consiste en incluir en el constructor estático del tipo genérico código que lance alguna excepción cuando los argumentos tipo especificados no sean válidos, abortándose así la expansión. Por ejemplo, un tipo genérico C<T> que sólo admita como argumentos tipos objetos de las clases A o B podría crearse como sigue:


 class C<T>
 {
  static C()
  {
  if ( !(typeof(T) == typeof(A)) && !(typeof(T) == typeof(B)))
   throw new ArgumentException("El argumento tipo para T debe ser A o B");
  }
 }

    O si se quisiese que también los admitiese de cualquier clase derivada de éstas, podría  aprovecharse como sigue el método bool IsAssignableForm(Type tipo) de los objetos Type, que indica si el objeto al que se aplica representa a un tipo al que se le pueden asignar objetos del tipo cuyo objeto System.Type se le indica como parámetro:


 class C<T>
 {
  static C()
  {
  if ( !(typeof(B).IsAssignableFrom(typeof(T))) &&
    !(typeof(A).IsAssignableFrom(typeof(T))))
    throw new ArgumentException("El argumento tipo para T debe ser A o B");
  }
 }

    El único problema de esta forma de restringir el abanico de tipos es que desde el código del tipo genérico no se podrá acceder directamente a los miembros específicos del tipo argumento, sino que antes habrá que convertirlos explícitamente a su tipo. Por ejemplo:


 using System;
 public class A
 {
  public void Método()
  {
     Console.WriteLine("Ejecutado Método() de objeto A");
  }
 }
 public class B
 {
  public void Método()
  {
     Console.WriteLine("Ejecutado Método() de objeto B");               
  }
 }
 class C<T> 
 {
    static C()
    {
      if ( !(typeof(T) == typeof(A)) && !(typeof(T) == typeof(B)))
         throw new ArgumentException("El argumento tipo para T debe
ser A o B"
);
    }
    public void LlamarAMétodo(T objeto)
    {
    if (objeto is A)                          
      (objeto as A).Método(); // Hay que hacer conversión explícita
    else
      (objeto as B).Método(); // Hay que hacer conversión explícita
    }
 }
 class Principal
 {
    static void Main()
    {
    C<A> objetoCA = new C<A>();
    objetoCA.LlamarAMétodo(new A());
    C<B> objetoCB = new C<B>();
    objetoCB.LlamarAMétodo(new B());
    }
 }

Valores por defecto

    Cuando se trabaja con tipos genéricos puede interesar asignarles el valor por defecto de su tipo, pero… ¿cuál será éste? Es decir, si el parámetro genérico se sustituye por un tipo referencia, el valor por defecto de sus objetos sería null y valdrían códigos como:


 void F<T>()
 {
  T objeto = null ;
 }

    Sin embargo, si se sustituyese por un tipo valor no sería válido, por lo que este tipo de asignaciones nunca se admitirán salvo que haya establecido una restricción de clase base que asegure que el argumento tipo sea siempre un tipo referencia.

    No obstante, C# 2.0 permite representar el valor por defecto de cualquier parámetro tipo con la sintaxis default(<parámetroTipo>), lo que dejaría al ejemplo anterior como sigue:


 void F<T>()
 {
  T objeto = default(T);
 }

    En cada expansión, el CLR sustituirá la expresión default(T) por el valor por defecto del tipo que se concrete para T (0, null, false,...), dando lugar a métodos como:

Expansión para int

Expansión para string

Expansión para bool


void F<int>()
{
  int objeto =0;
}


void F<string>()
{
  int objeto = null;
}


void F<bool>()
{
  int objeto = false;
}

    Nótese pues que para comprobar si un cierto argumento tipo es un tipo valor o un tipo referencia bastará con comprobar si su default devuelve null. Así por ejemplo, para crear un tipo genérico que sólo admita tipos referencia se podría hacer:


 class GenéricoSóloReferencias<T>
 {
  static GenéricoSóloReferencias()
  {
     if (default(T)!=null)
      throw new ArgumentException("T debe ser un tipo referencia");
  }
 }

    Por su parte, y a diferencia de lo que ocurre con el operador de asignación, el operador de igualdad (==) sí que puede aplicarse entre instancias de parámetros tipo y null, aún cuando estos almacenen tipos valor y dicha comparación no sea válida. En dichos casos la comparación simplemente retornaría false. Igualmente, también se les podrá aplicar el operador de desigualdad (!=), que siempre devolverá true para los tipo valor.

Ambigüedades

    El que los delimitadores de los argumentos tipo sean los mismos que los operadores de comparación "mayor que" y "menor que" y utilicen como el mismo carácter separador que se usa para separar los argumentos de las llamadas a métodos (la coma ,) puede dar lugar a ambigüedades. Por ejemplo, una llamada como la siguiente:


F(G<A,B>(7));

    Podría interpretarse de dos formas: Como una llamada a un método F() con el resultado de evaluar G<A como primer argumento y el resultado de B>(7) como segundo, o como una llamada a F() con el resultado de una llamada G<A,B>(7) a un método genérico G()

    Lo que hace el compilador es interpretar como llamadas a métodos genéricos cualquier aparición del carácter > donde a este le sigan alguno de estos caracteres: (, ), ], >, ;, :, ,?, . ó ,. En el resto de casos serán tratados como operadores de comparación. Por tanto, en el ejemplo anterior la expresión será interpretada como una llamada a un método genérico, y para que se interpretase de la otra forma habría rescribirse como F(G<A,B>7);

Genéricos
José Antonio González Seco

José Antonio es experto en tecnologias Microsoft. Imparte cursos y conferencias en congresos sobre C# y .NET en Universidades de toda España (Sevilla, Barcelona, San Sebastián, Valencia, Oviedo, etc.) en representación de grandes empresas como Microsoft.
Fecha de alta:11/12/2006
Última actualizacion:11/12/2006
Visitas totales:27507
Valorar el contenido:
Últimas consultas realizadas en los foros
Últimas preguntas sin contestar en los foros de devjoker.com