Inicio > .NET Framework, C#, Tutorial > Todo lo que siempre quisiste saber sobre colecciones y tenías miedo de preguntar… Enumeraciones

Todo lo que siempre quisiste saber sobre colecciones y tenías miedo de preguntar… Enumeraciones


El tema de las colecciones es enorme. Las colecciones pululan por doquier en el .NET Framework y ciertamente el dominar las decenas de clases que existen (o al menos las principales) se convierte en un requisito indispensable para escribir aplicaciones poderosas y eficientes.

Una colección, en esencia, es un conjunto de objetos. Cómo se almacenen éstos, depende del tipo de colección en cuestión. A su vez, una colección puede permitir cierto tipo de operaciones sobre el conjunto, como agregar o eliminar elementos, buscarlos, accederlos de forma secuencial o de alguna otra manera. Sin embargo, una cosa en común tienen las colecciones: todas se pueden enumerar, de una u otra forma. Así pues, comencemos esta entrada revisando lo que es una enumeración.

Una enumeración (no confundir con objetos “enum”) es una forma a través de la cual obtenemos todos los elementos de un conjunto de objetos determinado. Un enumerador (también llamado iterador) es un algoritmo que se encarga de obtener la enumeración para el dicho conjunto.

Por ejemplo, dado el conjunto {x | x > 0 && x < 10 }, si enumeramos sus elementos obtendríamos 1, 2, 3, 4, 5, 6, 7, 8 y 9. Por supuesto, el orden en el que se obtiene es indistinto, al menos desde el punto de vista del enumerador: lo único que se requiere es que se enumeren todos los elementos. Así, algún otro enumerador podría haber obtenido la secuencia 9, 1, 8, 2, 7, 3, 6, 4, 5 y sería igual de válido.

El .NET Framework incluye algunas interfaces para definir cuándo una clase es enumerable o cuándo es un enumerador. Desde el punto de vista .NET, cualquier clase es enumerable si implementa la interfaz IEnumerable (definida en System.Collections), y cualquier clase es un enumerador si implementa la interfaz IEnumerator. De hecho, la monada de una clase enumerable es que define un método llamado GetEnumerator, que devuelve el enumerador en cuestión…

Para tratar este tema un poco más a fondo, consideremos el siguiente ejemplo. Supongamos que tenemos una clase llamada Employee que define algunas propiedades.

public class Employee
{
  public Employee()
  {
    Id = string.Empty;
    Password = string.Empty;
    Name = string.Empty;
    Salary = 0F;
  }

  public string Id { get; set; }
  public string Password { get; set; }
  public string Name { get; set; }
  public float Salary { get; set; }
}

Sencillita nomás. Ahora supongamos que tenemos alguna rutina que recibe como parámetro un objeto y necesita listar las propiedades que expone. Una forma de solucionar este problema sería hacer que Employee enumerara las propiedades. Y qué mejor que utilizar un enumerador.

Así pues, necesitamos crear una clase que implemente IEnumerator y que devuelva las propiedades de un objeto Employee. Pero antes de ver algo de código, conviene hacer una revisión de la interfaz IEnumerator y ver un poquito de teoría

Esta interfaz sigue el patrón de diseño conocido como iterador. Un iterador debe poder obtener un elemento de un conjunto de objetos y debe poder navegar hacia el siguiente, mientras todavía queden elementos sobre los que todavía no se haya iterado. Adicionalmente debe poderse restablecer al estado inicial, es decir, después de restablecerse, el siguiente elemento que se obtenga será el primero que se obtuvo previamente.

La interfaz IEnumerator define precisamente este comportamiento. Ésta contiene el método Reset, que como cabe esperar restablece el estado de iteración para que apunte al primer elemento de la enumeración. Cuenta también con el método MoveNext, que avanza hacia el siguiente elemento de la enumeración, y devuelve true si todavía existen elementos, o false si ya no hay más elementos que enumerar (en cuyo caso, se tendría que llamar a Reset para volver al inicio). Finalmente, la propiedad Current nos devuelve un objeto que es, ni más ni menos, que el elemento actual de la enumeración.

De esta forma, tenemos que el proceso para enumerar consiste en los siguientes pasos:

  1. Obtener una instancia de IEnumerable.
  2. Obtener un IEnumerator llamando al método GetEnumerator() del objeto IEnumerable.
  3. Iterar en algún bucle (digamos, un while) llamando a MoveNext del objeto IEnumerator.
  4. Obtener el valor actual con la propiedad Current del objeto IEnumerator.
  5. Cuando ya no quede más (i.e. MoveNext regresa false), reestablecer la enumeración llamando a Reset del objeto IEnumerator.

    Alternativamente, podrías ejecutar el paso 5 inmediatamente después del paso 2, para asegurarte que el enumerador siempre apunte al primer elemento. Pero bueno, nos quedamos con la versión original de momento. En código sería algo así.

    IEnumerable enumerable = ...; // obtener algún IEnumerable de algún lado
    IEnumerator enumerator = enumerable.GetEnumerator();
    while (enumerator.MoveNext())
    {
      object element = enumerator.Current;
      // hacer algo con element
    }
    enumerator.Reset();
    

Obviamente, si estamos enumerando cadenas de texto, tendríamos que hacer la conversión de enumerator.Current a string, o bien al tipo de dato que enumeremos.

Afortunadamente, C# cuenta con un poco de “azúcar sintáctica” y nos provee una construcción mucho más sencilla de utilizar, y que a final de cuentas el compilador transforma en un código similar al anterior. Me refiero al bucle foreach. El siguiente ejempo muestra cómo le haríamos si enumeráramos cadenas de texto.

IEnumerable elements = ...; //obtener algún objeto enumerable
foreach (string element in elements)
{
  // hacer algo con element
}

El bucle foreach incluso se encarga de convertir IEnumerator.Current al tipo de dato que le especifiquemos (en este caso, string). Así las cosas, vemos que podemos utilizar el foreach sobre cualquier objeto cuya clase implemente IEnumerable (y también si implementa IEnumerator, el compilador se encarga del trabajo de distinguir entre ambas).

Pero bueno, ya fue mucha teoría, regresemos a nuestra clase Employee. Decíamos que tenemos que crear una clase que implemente IEnumerator, y por ende, tenemos que implementar un método Reset, un método MoveNext y una propiedad Current. La lógica de cómo le hagamos para regresar los elementos depende enteramente de nosotros. Lo que vamos a hacer es tener una variable llamada _currentProperty, que guarda el nombre de la propiedad actual. Cuando hagamos un MoveNext, nos movemos a la siguiente propiedad y le asignamos el nombre de ésta. Así, la secuencia comienza con string.Empty, que indica nuestro estado inicial. Al hacer el MoveNext, nos pasamos a Id, y luego a Name, Password y Salary, donde terminamos la iteración regresando false.

Echémosle un vistazo al siguiente código.

class EmployeeEnumerator : IEnumerator
{
  private Employee _employee;
  private object _current;
  private string _currentProperty;

  public EmployeeEnumerator(Employee employee)
  {
    if (employee == null)
      throw new ArgumentNullException("employee");

    _employee = employee;
    Reset();
  }

  public object Current
  {
    get { return _current; }
  }

  public bool MoveNext()
  {
    bool canMove = false;

    if (_currentProperty == string.Empty)
    {
      _current = _employee.Id;
      _currentProperty = "Id";
      canMove = true;
    }
    else if (_currentProperty == "Id")
    {
      _current = _employee.Name;
      _currentProperty = "Name";
      canMove = true;
    }
    else if (_currentProperty == "Name")
    {
      _current = _employee.Password;
      _currentProperty = "Password";
      canMove = true;
    }
    else if (_currentProperty == "Password")
    {
      _current = _employee.Salary;
      _currentProperty = "Salary";
      canMove = true;
    }

    return canMove;
  }

  public void Reset()
  {
    _current = _employee.Id;
    _currentProperty = string.Empty;
  }
}

Como puedes ver, la lógica está de nuestro lado y por supuesto que es bastante mala, pero a final de cuentas funciona. Ahora, solo nos resta hacer que nuestra clase Employee implemente IEnumerable, para lo cual le creamos un método GetEnumerator que nos devuelva una instancia de EmployeeEnumerator.

class Employee : IEnumerable
{
  public Employee()
  {
    Id = string.Empty;
    Password = string.Empty;
    Name = string.Empty;
    Salary = 0F;
  }

  public string Id { get; set; }
  public string Password { get; set; }
  public string Name { get; set; }
  public float Salary { get; set; }

  public IEnumerator GetEnumerator()
  {
    return new EmployeeEnumerator(this);
  }
}

Y ahora sí, podemos ejecutar un bucle foreach sobre una instancia de Employee, como se muestra en el siguiente ejemplo.

static void Main(string[] args)
{
  Employee employee = new Employee();
  employee.Id = "fgomez";
  employee.Password = "P@ssw0rd";
  employee.Name = "Fernando Gómez";
  employee.Salary = 2000f;

  IEnumerable enumerable = employee;
  IEnumerator enumerator = enumerable.GetEnumerator();
  while (enumerator.MoveNext())
    Console.WriteLine(enumerator.Current.ToString());
  enumerator.Reset();

  Console.ReadKey(true);
}

Al ejecutar este código, obtendríamos la siguiente salida por la consola.

fgomez
Fernando Gómez
P@ssw0rd
2000

Alternativamente, podríamos haber utilizado algo más sencillo como un foreach y hubiéramos obtenido el mismo resultado.

static void Main(string[] args)
{
  Employee employee = new Employee();
  employee.Id = "fgomez";
  employee.Password = "P@ssw0rd";
  employee.Name = "Fernando Gómez";
  employee.Salary = 2000f;

  foreach (object element in employee)
    Console.WriteLine(element.ToString());

  Console.ReadKey(true);
}

Las enumeraciones son la base de las colecciones, ya que toda colección debe permitir acceder a sus elementos de una u otra forma, así que no es sorprendente que toda colección implemente IEnumerable. Es por ello que rara vez tendremos que implementar IEnumerable o IEnumerator nosotros mismos.

Sin embargo, si lo quisiéramos hacer –como en este ejemplo—nos toparíamos con el hecho de que llevar la lógica sobre cómo construir un IEnumerator es un poco complicada. Eso de ir asignando variables y así pues es complicado y por lo mismo, propenso a errores. Si me equivoco en la lógica de asignación puedo provocar un bucle infinito que terminará por matar la aplicación. Para ayudar a solventar este problema, C# tiene una palabra reservada que nos permite construir objetos IEnumerator de forma por demás sencilla. Esta palabra reservada se llama “yield return”.

Tradicionalmente, “return” hace que regresemos un valor específico como valor de retorno de una función y al momento finaliza la ejecución del método sin importar si todavía quedan instrucciones por ejecutarse. La expresión “yield return” lo que hace es que captura el valor devuelto, pero no aborta la ejecución del método, sino que sigue adelante hasta terminar el método o hasta encontrar una sentencia “return”. Así, podemos ejecutar varios “yield return” en un solo método, y cada vez que se hace el valor en cuestión se captura y se va guardando. Cuando termina el método de ejecutarse, éste devolverá un objeto de tipo IEnumerable, con su IEnumerator propiamente construido. Es decir, el compilador hace todo el trabajo sucio y propenso a errores por nosotros. Así, ya no tenemos que implementar IEnumerator en alguna clase, sino que simplemente implementamos IEnumerable y en la implementación de GetEnumerator, hacemos un “yield return” de todos los elementos que queramos enumerar.

Debido a lo anterior, podríamos reescribir todo nuestro código de la siguiente forma.

using System;
using System.Collections;

namespace Fermasmas.Wordpress.Com
{
  class Employee : IEnumerable
  {
    public Employee()
    {
      Id = string.Empty;
      Password = string.Empty;
      Name = string.Empty;
      Salary = 0F;
    }

    public string Id { get; set; }
    public string Password { get; set; }
    public string Name { get; set; }
    public float Salary { get; set; }

    public IEnumerator GetEnumerator()
    {
      yield return Id;
      yield return Name;
      yield return Password;
      yield return Salary;
    }
  }

  class Program
  {
    static void Main(string[] args)
    {
      Employee employee = new Employee();
      employee.Id = "fgomez";
      employee.Password = "P@ssw0rd";
      employee.Name = "Fernando Gómez";
      employee.Salary = 2000f;

      foreach (object element in employee)
        Console.WriteLine(element.ToString());

      Console.ReadKey(true);
    }

  }
}

Este código produce el mismo resultado que los anteriores, pero puedes notar dos diferencias. En primer lugar, ya no necesitamos la clase EmployeeEnumerator, y en segunda, la implementación de GetEnumerator de IEnumerable hace uso del “yield return” para regresar todas las propiedades. Esto es mucho más elegante que como le hacíamos, además de que escribimos menos y nuestro código ya es menos propenso a errores.

En conclusión… Las enumeraciones no forman parte de una colección. Pero cualquier colección es enumerable. Por ello decidí comenzar este tutorial hablando de ellas. Como decía, rara vez tendrás que implementar a manita alguna enumeración (y aún si tienes que hacerlo siempre puedes facilitarte la vida con “yield return”), pero es importante saber cómo funcionan para que puedas comprender mejor las colecciones. En la siguiente entrada de este tutorial ya nos encargaremos de hablar de colecciones, y en posteriores entradas veremos los diferentes tipos de colecciones que .NET Framework nos ofrece. Pero por hoy ha sido suficiente, y ya hay que ir a dormir, que ya casi son las tres de la mañana.

Así que aquí le paramos. ¡Hasta la próxima!

Anuncios
Categorías:.NET Framework, C#, Tutorial Etiquetas: , ,
  1. Aryam
    junio 17, 2010 en 10:39 pm

    Excelente Blog…, estoy empezando a utilizar las enumeraciones y tu explicación, es muy buena…, te felicito…!!!

    Seguiré visitando tus publicaciones.

    Saludos.

    • junio 18, 2010 en 9:55 am

      ¡Muchas gracias por tus comentarios Aryam!

  2. alex
    junio 27, 2014 en 12:55 pm

    gracias, aun dudo sobre las utilidades practicas pero me encontraré algun dia por necesidad usando esto.

    • junio 27, 2014 en 10:04 pm

      Hola! Pues siempre que utilices un foreach estás usando un IEnumerator/IEnumerable… cualquier array implementa estas interfases, están por todos lados!
      Saludos!

  1. junio 5, 2010 en 12:08 am
  2. noviembre 23, 2012 en 6:43 am

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s