Firmas invitadas |
La interfaz IEnumerable<T>
Publicado el 20/Feb/2007
|
1. IntroducciónPara una comprensión cabal de las posibilidades que LINQ pondrá a disposición de los programadores con la aparición de la próxima versión de .NET, es necesario dominar varios recursos lingüísticos sobre los que esta tecnología se apoya en gran medida, que ya fueron incluidos en .NET Framework y los lenguajes Microsoft para la plataforma (C# y Visual Basic) con la aparición de .NET 2.0 [2]. Estos recursos son: · Los genéricos, que posibilitan la definición de tipos y métodos parametrizados con respecto a uno o más tipos de datos. · Los iteradores, que hacen posible la especificación concisa de mecanismos para la iteración perezosa o bajo demanda sobre los elementos de una secuencia. · Los métodos anónimos, que permiten especificar en línea el código de un método al que hará referencia un delegado. En una columna anterior ya hemos presentado las expresiones lambda, una manera alternativa de crear métodos anónimos que estará disponible en C# 3.0 [3]. · Los tipos anulables, que hacen posible utilizar la semántica del valor nulo propia de los tipos referencia también sobre los tipos valor.
A lo largo de esta entrega y la siguiente presentaremos con cierto nivel de detalle dos interfaces genéricas que juegan un papel crucial en LINQ [4, 5]: IEnumerable<T> e IQueryable<T>.
2. La interfaz IEnumerable<T>La interfaz genérica IEnumerable<T> (espacio de nombres System.Collections.Generic) fue introducida en .NET 2.0 con el objetivo básico de jugar para los tipos genéricos el papel que cumplía en .NET 1.x la interfaz System.Collections.IEnumerable: el de ofrecer un mecanismo para la iteración sobre los elementos de una secuencia, generalmente con la vista puesta en aplicar a esa secuencia el patrón de programación foreach. En lo que respecta a LINQ, la importancia de la interfaz IEnumerable<T> estriba en que cualquier tipo de datos que la implemente puede servir directamente como origen para expresiones de consulta. En particular, los arrays y las colecciones genéricas .NET 2.0 la implementan. La definición de IEnumerable<T> es la siguiente: // System.Collections.Generic public interface IEnumerable<T> : IEnumerable { IEnumerator<T> GetEnumerator(); } // System.Collections public interface IEnumerable { IEnumerator GetEnumerator(); }
Como puede verse, la interfaz incluye un único método GetEnumerator(), que devuelve un enumerador – un objeto cuyo objetivo es generar elementos secuencialmente. Para hacer posible el recorrido de colecciones genéricas desde código no genérico, IEnumerable<T> hereda de su homóloga no genérica, IEnumerable, y por tanto debe implementar también una versión no genérica de GetEnumerator(), para la que generalmente sirve el mismo código de la versión genérica. Por su parte, la interfaz IEnumerator<T> está definida de la siguiente forma: // System.Collections.Generic public interface IEnumerator<T> : IDisposable, IEnumerator { T Current { get; } } // System.Collections public interface IEnumerator { object Current { get; } void Reset(); bool MoveNext(); }
Nuevamente, la interfaz se apoya en su contrapartida no genérica. En conjunto, IEnumerator<T> debe implementar los siguientes miembros:
La clase que implemente IEnumerator<T> deberá encargarse de mantener el estado necesario para garantizar que los métodos de la interfaz funcionen correctamente. ¿Por qué esta separación en dos niveles, en la que básicamente IEnumerable<T> es de un nivel más alto, mientras que IEnumerator<T> se encarga del “trabajo sucio”? ¿Por qué no dejar que las colecciones implementen directamente IEnumerator<T>? La respuesta tiene que ver con la necesidad de permitir la ejecución de iteraciones anidadas sobre una misma secuencia. Si la secuencia implementara directamente la interfaz enumeradora, solo se dispondría de un “estado de iteración” en cada momento y sería imposible implementar bucles anidados sobre una misma secuencia, como por ejemplo los que se encuentran en la implementación típica de la ordenación mediante el algoritmo de la burbuja. En vez de eso, las secuencias implementan IEnumerable<T>, cuyo método GetEnumerator() debe producir un nuevo objeto de enumeración cada vez que es llamado.
3. La semántica de foreach para IEnumerable<T>Con la explicación anterior, debe quedar más o menos clara cuál es la semántica del bucle foreach cuando se aplica a objetos que implementan IEnumerable<T>. Se basa en obtener un enumerador llamando a GetEnumerator(), para entonces recorrerlo de principio a fin utilizando su método MoveNext(). Un método para aplicar una misma acción sobre todos los elementos de una secuencia enumerable genérica sería:
delegate void M<T>(T t); static void Iterate<T>(IEnumerable<T> secuencia, M<T> metodo) { foreach(T t in secuencia) metodo(t); }
Su equivalente sin utilizar foreach sería: static void Iterate<T>(IEnumerable<T> secuencia, M<T> metodo) { { IEnumerator<T> e = secuencia.GetEnumerator(); try { e.Reset(); while (e.MoveNext()) { T t = e.Current; metodo(t); } } finally { e.Dispose(); } } }
A continuación se presenta un ejemplo de cómo utilizar este método para imprimir una secuencia de enteros. Observe la utilización de una expresión lambda [3] como alternativa a la instanciación del tipo delegado correspondiente: static void Main(string[] args) { List<int> intList = new List<int>() { 1, 3, 5 }; Iterate<int>(intList, (int x) => Console.WriteLine(x)); }
4. Ejemplo de implementación de IEnumerable<T>Aunque no es algo que sea necesario realizar con frecuencia, vamos a continuación a programar desde cero una clase que implemente IEnumerable<T> (en dos variantes), lo que ayudará al lector a comprender mejor la complejidad asociada a la implementación de esta interfaz y a la vez a refrescar los conocimientos relacionados con los iteradores de C# 2.0. La clase que vamos a desarrollar nos permitirá iterar sobre la secuencia de los números naturales desde 1 hasta 1000, ambos inclusive. En la primera implementación, más “clásica”, se programan de manera explícita todos los métodos de las interfaces IEnumerable<int> e IEnumerator<int>. Observe la definición de la clase del enumerador como una clase anidada, enfoque utilizado con bastante frecuencia en estos casos: public class NaturalNumbersSequence: IEnumerable<int> { public class NaturalEnumerator: IEnumerator<int> { private int current = 1; private bool atStart = true; // interface members public int Current { get { return current; } } object IEnumerator.Current { get { return current; } } public void Reset() { atStart = true; current = 1; } public bool MoveNext() { if (atStart) { atStart = false; return true; } else { if (current < 1000) { current++; return true; } else return false; } } public void Dispose() { // do nothing } } public IEnumerator<int> GetEnumerator() { return new NaturalEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return new NaturalEnumerator(); } }
La segunda versión se apoya en el concepto de iterador de C# 2.0 y ofrece una implementación equivalente pero mucho más concisa: public class NaturalNumbersSequence: IEnumerable<int> { public IEnumerator<int> GetEnumerator() { for (int i = 1; i <= 1000; i++) yield return i; } IEnumerator IEnumerable.GetEnumerator() { for (int i = 1; i <= 1000; i++) yield return i; } }
En esta versión, el compilador se encarga de sintetizar una clase muy similar a la clase NaturalEnumerator de la primera versión, y de construir y devolver un objeto de esa clase cuando se va comenzar la iteración. En cualquiera de los dos casos, un fragmento de código para iterar sobre esa secuencia de números tendría la misma apariencia: foreach(int i in new NaturalNumbersSequence()) Console.WriteLine(i);
5. La generación bajo demanda durante la iteraciónUn elemento a tener en cuenta en relación con IEnumerable<T> es el hecho de que, a menos que se trate de iterar sobre los elementos de un array o colección ya creados en memoria de antemano, posiblemente los diferentes elementos que componen una secuencia se irán generando en la medida en que vaya siendo necesario consumirlos, hecho que se conoce en el mundo de la programación como evaluación bajo demanda, diferida o perezosa (lazy). Por ejemplo, supongamos que hemos definido una clase enumerable PrimeSequence que produce la secuencia de los números primos: public class PrimeSequence: IEnumerable<int> { private IEnumerator<int> getEnumerator() { int i = 2; while (true) { if (i.IsPrime()) yield return i; i++; } } public IEnumerator<int> GetEnumerator() { return getEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return getEnumerator(); } }
Aquí para continuar practicando las nuevas posibilidades que ofrecerá C# 3.0 hemos creado el método IsPrime() como un método extensor [4]. El conjunto de los números primos es potencialmente infinito, por lo que sería simplemente impracticable generar de antemano todos los valores de la secuencia; por otro lado, es bien conocido que los números primos se van haciendo más “escasos” en la medida en que avanzamos a lo largo del eje numérico, por lo cual es más costoso generar cada nuevo valor. Sería ineficiente generar 10.000 números primos para luego consumir solo unos cuantos. Usando la clase enumerable que presentamos aquí, esos problemas dejan de ser relevantes: los números primos se irán generando en la medida en que el código cliente los vaya necesitando. Eso sí, en el ejemplo que nos ocupa, el código cliente deberá hacerse responsable de abortar la iteración, ya que el método MoveNext() sintetizado siempre devolverá true: // obtain primes less than 1000 foreach(int i in new PrimeSequence()) if (i > 1000) break; else Console.WriteLine(i);
6. ConclusionesEn este artículo hemos presentado la interfaz IEnumerable<T>, que es necesario conocer debido a la importancia que ya tiene y a la que adquirirá con la próxima aparición de la versión 3.0 de C#. En el próximo artículo hablaremos sobre IQueryable<T>, otra interfaz que jugará un papel crucial en la implementación de tecnologías como LINQ To SQL (la extensión de LINQ para el trabajo con bases de datos relacionales. El código fuente del ejemplo utilizado en el artículo está disponible para su descarga. Para poder compilarlo y ejecutarlo satisfactoriamente, se deberá instalar inicialmente la Presentación Preliminar de LINQ de Mayo de 2006, disponible en [1].
7. Referencias
|
Código de ejemplo (ZIP): |
Fichero con el código de ejemplo:
octavio_IEnumerable.zip - 5.74 KB
|