Archivo

Archive for 18 junio 2010

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


Cualquier programador que haya estudiado estructuras de datos conocerá las pilas y las colas. Junto con las listas enlazadas, los arrays y los diccionarios, forman el conjunto básico de estructuras de datos. El .NET Framework implementa clases para estas estructuras, y que son colecciones porque derivan de ICollection. Sin embargo, difieren en concepto e implementación de las listas y los diccionarios que hasta el momento hemos visto.

Definición

Una pila (stack, en inglés) es una estructura de datos a la que se le van agregando objetos y no cuenta con acceso aleatorio (es decir, a través de un indexador) sino secuencial. La secuencia en la que se obtienen los objetos almacenados es de la forma últimas entradas primeras salidas.

Considera un montón de monedas que queremos apilar. Conforme vamos colocando las monedas queda patente que la última que colocamos es la que está hasta arriba, es decir, la primera que vemos. Si añadimos una de un peso, una de dos, una de cinco y una de diez, y en este momento comenzamos a remover, las iríamos escogiendo en orden inverso a como fueron colocadas: primero la de diez pesos, luego la de cinco, la siguiente es la de dos y por último, la de un pesito. Este es precisamente el concepto de pila y más general, el concepto de últimas entradas y primeras salidas.

Nota también que al ir añadiendo monedas, una vez que coloco la moneda de dos pesos la de uno queda totalmente inaccesible, a menos que primero quite la de dos y ahora sí la de un peso. Esto ilustra una propiedad de las pilas: no cuentan con acceso aleatorio. Aunque de hecho en el ejemplo en cuestión sí podría obtener la moneda de uno, pero tendría que partir la pila en dos y remover de golpe las monedas que hay arriba, con el riesgo de que la pila puede caerse. Es decir, el proceso de hacerlo es costoso y la ventaja de tener la pila se pierde totalmente.

Una cola es muy similar a una pila. Tampoco cuentan con acceso aleatorio, sino secuencial. Esta secuencia sigue la forma de primeras entradas primeras salidas.

Un ejemplo que encaja a la perfección es la fila de personas frente a una taquilla de boletos en un estadio de Sudáfrica para el encuentro entre México y Francia (el cual, debo añadir orgullosamente, ganamos esta tarde por marcador de dos a cero). Conforme la gente va llegando a la fila, en ese orden se le vende el boleto de entrada: el vendedor atiende al primero que llegó, luego al que está atrás, etcétera. Un intento por saltar este orden terminaría en un grupo numeroso de aficionados enojados.

Estos conceptos son sencillos y sin embargo muy útiles, y por ello el .NET Framework les dedica especial atención al proveer dos clases en el espacio de nombres System.Collections: Stack para las pilas y Queue para las colas.

La clase Stack

Comencemos por la clase Stack. En primera instancia, esta clase implementa las interfaces IEnumerable, que nos permite enumerar los elementos de la pila, e ICollection, que de buenas a primeras nos permite contar el número de elementos, copiar el contenido a un array y tener un acceso sincronizado a los elementos.

Stack cuenta con tres constructores: uno por defecto, uno que toma como parámetro un ICollection y que agrega los elementos de dicha colección a la pila, y uno que toma un número entero indicando la capacidad inicial (recuerda que las colecciones crecen al paso del tiempo, y cada vez que crecen tienen que aumentar su tamaño en memoria; por ende si sabemos anticipadamente cuántos elementos tendrá la colección podemos especificar la capacidad para no tener que estar haciendo redimensionamiento, obteniendo un mejor rendimiento del procesador).

Posteriormente contamos con algunos métodos que nos son familiares. Clear elimina todo el contenido de la pila, Contains nos indica si la colección contiene un objeto determinado, CopyTo copia el contenido de la pila aun array y ToArray regresa un array con los elementos que contiene la pila.

Pero quizás los métodos más importantes sean Push y Pop. Push es muy similar al Add de las listas: nos permite añadir un elemento a la pila, mientras que Pop regresa el último elemento añadido (recuerda: últimas entradas primeras salidas) y lo elimina de la colección. El siguiente ejemplo crea un objeto Stack, le añade algunos elementos usando Push y luego los obtiene usando Pop.

Stack stack = new Stack(10);
stack.Push("Carlos Salcido");
stack.Push("Rafael Márquez");
stack.Push("Javier Hernández");
stack.Push("Giovani Dos Santos");
stack.Push("Carlos Vela");

while (stack.Count > 0)
{
    string mexicanPlayer = stack.Pop() as string;
    Console.WriteLine(mexicanPlayer);
}

Console.ReadKey(true);

Al ejecutar este programa obtenemos la siguiente salida en la consola.

Carlos Vela
Giovani Dos Santos
Javier Hernández
Rafael Márquez
Carlos Salcido

Como era de esperarse, la salida de los nombres de algunos jugadores de la Selección Mexicana de Fútbol se da en el orden inverso al que fueron añadidos: últimas entradas primeras salidas.

Ahora bien, al hacer uso de Pop obtenemos el elemento pero también lo removemos de la colección. Esto puede convertirse en un lío cuando solo queramos leer el siguiente elemento, sin removerlo de la colección. Para solventar este problema, contamos con el método Peek. Este método devuelve el mismo elemento que devolvería Pop, pero sin eliminarlo de la colección.

Stack stack = new Stack(10);
stack.Push("Carlos Salcido");
stack.Push("Rafael Márquez");
stack.Push("Javier Hernández");
stack.Push("Giovani Dos Santos");
stack.Push("Carlos Vela");

string mexicanPlayer = stack.Peek() as string;
Console.WriteLine("Jugador: {0}", mexicanPlayer);
Console.WriteLine();

foreach (string player in stack)
    Console.WriteLine(player);

En este ejemplo, usamos Peek para obtener el siguiente jugador de la pila (es decir, el último) que en este caso será Carlos Vela. Luego hacemos un foreach, que imprime a los cinco jugadores, demostrando cómo Peek no elimina el elemento de la colección.

La clase Queue

La clase Queue representa una cola, y es muy similar a la clase Stack. También implementa IEnumerable e ICollection y tiene prácticamente los mismos métodos que Stack. Sin embargo, en lugar de Push y Pop, esta clase cuenta con los métodos Enqueue y Dequeue para añadir un elemento a la fila y para obtener y remover el primer elemento de la cola (recuerda: primeras entradas, primeras salidas. Por lo demás, las clases son prácticamente idénticas.

Queue queue = new Queue(5);
queue.Enqueue("Carlos Salcido");
queue.Enqueue("Rafael Márquez");
queue.Enqueue("Javier Hernández");
queue.Enqueue("Giovani Dos Santos");
queue.Enqueue("Carlos Vela");

while (queue.Count > 0)
{
    string mexicanPlayer = queue.Dequeue() as string;
    Console.WriteLine(mexicanPlayer);
}

Como cabría esperar, este código imprime los nombres de los jugadores en el orden en el que fueron añadidos a la colección.

Carlos Salcido
Rafael Márquez
Javier Hernández
Giovani Dos Santos
Carlos Vela

Un método extra que tiene Queue pero no tiene Stack es el método TrimToSize. Este método redimensiona el bloque de memoria interna para que su tamaño sea equivalente al número de elementos de la colección. Esto es útil en caso de que tengamos una cola con muchos elementos, digamos mil, y removemos una gran cantidad, digamos quinientos. Aunque removamos los elementos, la cola seguirá almacenando suficiente memoria para mil elementos, convirtiéndose en un desperdicio de memoria. Si sabemos que por el momento la cola no aumentará de tamaño, sería bueno invocar a TrimToSize para liberar la memoria reservada.

Y bueno, eso es todo con respecto a estas clases. Realmente son muy sencillas y su manejo no debería darnos problemas.

Escenarios de uso

Quizás no te quede claro bajo qué circunstancias podríamos emplear alguna de estas clases. Pues bien, la parte final de esta entrada te mostrará algunos escenarios de uso para las pilas y colas.

Mensajes

Quizás una de las aplicaciones de las colas más comunes son los mensajes. Considera este escenario: tienes un método que realiza ciertas operaciones y cada operación puede lanzar un mensaje que deberá ser procesado por otra entidad en el orden en el que fueron creados.

El sistema gráfico de Windows es un ejemplo de ello. Cada vez que se genera algún evento en la interfaz gráfica (un clic del ratón o una tecla pulsada) se genera un Windows Message (WM). Cada WM se almacena en una cola, y hasta que el sistema operativo le pasa el control a la aplicación, ésta deberá responder uno a uno a los mensajes enviados, conforme fueron generados. Esta forma de trabajo se hace en forma paralela, así que múltiples hilos entran en juego.

El siguiente código emula un sistema de mensajes donde un hilo se encarga de generar dichos mensajes mientras que el hilo principal está a la espera de éstos. Cuando encuentra un mensaje, lo imprime en la consola, y si no encuentra nada entonces se duerme algunas décimas de segundo antes de volver a revisar la cola por si hay más mensajes disponibles.

using System;
using System.Collections;
using System.Threading;

namespace Fermasmas.Wordpress.Com
{
    class Program
    {
        // este es nuestra cola de mensajes. 
        static volatile Queue _messages;

        static void Main(string[] args)
        {
            _messages = new Queue();

            // inicializamos el generador de mensajes en un hilo aparte.
            Thread thread = new Thread(new ThreadStart(MessagePump));
            thread.Start();

            // revisamos la cola de mensajes mientras el hilo trabajador esté 
            // activo o existan mensajes por procesar. 
            while (thread.IsAlive || _messages.Count > 0)
            {
                if (_messages.Count > 0)
                {
                    // hay que bloquear la cola de mensajes por si el otro 
                    // hilo intenta añadir más. de no hacer esto podríamos 
                    // tener problemas de sincronización. 
                    lock (_messages.SyncRoot)
                    {
                        // removemos el mensaje y lo imprimimos en la consola.
                        string message = _messages.Dequeue() as string;
                        Console.WriteLine(message);
                    }
                }
                else
                {
                    // no hay mensajes todavía, dormimos el hilo principal
                    // antes de continuar. 
                    Console.WriteLine("Durmiendo...");
                    Thread.Sleep(500);
                }
            }

            // el hilo trabajador terminó de hacer su proceso y ya no hay más
            // mensajes por mostrar, así que es hora de terminar la 
            // aplicacón. 
            Console.WriteLine("Terminado...");
            Console.ReadKey(true);
        }

        static void MessagePump()
        {
            for (int i = 0; i < 10; i++)
            {
                Thread.Sleep(300);

                string message = string.Format("Mensaje {0}", i + 1);
                lock (_messages.SyncRoot)
                {
                    _messages.Enqueue(message);
                }
                
            }
        }
    }
}

La salida de este programa es similar a la siguiente.

Durmiendo...
Mensaje 1
Durmiendo...
Mensaje 2
Mensaje 3
Durmiendo...
Mensaje 4
Durmiendo...
Mensaje 5
Mensaje 6
Durmiendo...
Mensaje 7
Mensaje 8
Durmiendo...
Mensaje 9
Durmiendo...
Mensaje 10
Terminado...

Rastreo de actividades

Quizás haz notado que las excepciones en .NET cuentan con la propiedad StackTrace. Esta propiedad devuelve una cadena de texto que contiene los nombres de las funciones que se fueron ejecutando. Esto es de invaluable ayuda al depurar el programa ya que podemos darnos una idea sobre cómo se ejecutó el programa.

Pues bien, este rastreo se puede implementar utilizando una pila. Al iniciar un método, haces un Push con el nombre del método, y al terminarlo haces un Pop. De tal suerte, si surge una excepción, la pila sabrá exactamente en qué método te encuentras. No en vano se le llama StackTrace (o rastreo de pila).

El siguiente programa muestra una serie de funciones que se llaman entre sí. Al inicio, el programa le pide al usuario que escriba el nombre de la función que lanzará una excepción. Así, si escribo “Foo7” la función Foo7 lanzará una excepción cuando sea invocada. Dado que las funciones Foo1 a Foo7 se llaman entre sí, podremos ver cómo se hace el rastreo al guardar información sobre el método que se llama. Cuando se lanza la excepción, se muestra el rastreo que hacemos a mano y se compara contra el rastreo que hace la clase Exception.

using System;
using System.Collections;

namespace Fermasmas.Wordpress.Com
{
  class Program
  {
    static Stack _log;
    static string _failure;

    static void Main(string[] args)
    {
      _log = new Stack();

      Console.WriteLine("¿En qué función fallará?");
      _failure = Console.ReadLine();
      Console.Clear();

      try
      {
        Console.WriteLine("Iniciando proceso...");
        Foo1();
        Console.WriteLine("Terminando proceso...");
      }
      catch (Exception ex)
      {
        Console.WriteLine("Hubo un error... rastreo habilitado: ");
        while (_log.Count > 0)
        {
          Console.WriteLine("en {0}", _log.Pop());
        }
        Console.WriteLine();

        Console.WriteLine("Rastreo de Exception.StackTrace: ");
        Console.WriteLine(ex.StackTrace);
      }

      Console.ReadKey(true);
    }

    static void DoWork()
    {
      string current = _log.Peek() as string;
      if (current.IndexOf(_failure) >= 0)
      {
        _log.Push("DoWork... ");
        throw new Exception();
      }
    }

    static void Foo1()
    {
      _log.Push("Estamos en Foo1");

      DoWork();
      Foo2();
      Foo3();
      Foo4();

      _log.Pop();
    }

    static void Foo2()
    {
      _log.Push("Estamos en Foo2");
      DoWork();
      _log.Pop();
    }

    static void Foo3()
    {
      _log.Push("Estamos en Foo3");
      DoWork();
      Foo5();
      Foo6();
      _log.Pop();
    }

    static void Foo4()
    {
      _log.Push("Estamos en Foo4");
      DoWork();
      Foo5();
      _log.Pop();
    }

    static void Foo5()
    {
      _log.Push("Estamos en Foo5");
      DoWork();
      _log.Pop();
    }

    static void Foo6()
    {
      _log.Push("Estamos en Foo6");
      DoWork();
      Foo7();
      _log.Pop();
    }

    static void Foo7()
    {
      _log.Push("Estamos en Foo7");
      DoWork();
      _log.Pop();
    }
  }
}

Al ejecutar este programa, y decirle que quiero una excepción en Foo5, esta es la salida. Al comparar el rastreo manual que hacemos junto con el que hace Exception.StackTrace, veremos que son prácticamente idénticos.

Iniciando proceso...
Hubo un error... rastreo habilitado:
en DoWork...
en Estamos en Foo6
en Estamos en Foo3
en Estamos en Foo1

Rastreo de Exception.StackTrace:
   at Fermasmas.Wordpress.Com.Program.DoWork() in C:\[...]\Program.cs:line 47
   at Fermasmas.Wordpress.Com.Program.Foo6() in C:\[...]\Program.cs:line 97
   at Fermasmas.Wordpress.Com.Program.Foo3() in C:\[...]\Program.cs:line 75
   at Fermasmas.Wordpress.Com.Program.Foo1() in C:\[...]\Program.cs:line 57
   at Fermasmas.Wordpress.Com.Program.Main(String[] args) in C:\[...]\Program.cs:line 22

 

Deshacer acciones

Otro de los ejemplos clásicos sobre aplicaciones de pilas es el hacer y deshacer acciones. Editores de texto como Word o el Visual Studio cuentan con esta funcionalidad para deshacer el texto previamente escrito. Esta funcionalidad se puede fácilmente alcanzar con una pila. Conforme ingresamos texto, lo agregamos a la pila, y al hacer el "undo” tomamos el último elemento escrito (es decir, hacemos un Pop) y lo eliminamos del editor de texto. Hacer un “redo” es muy similar: lo que sacamos del “undo” lo metemos en otra pila y listo.

El siguiente programa es un pequeño editor de texto estilo vim y emacs, muy básico, que soporta la funcionalidad de hacer y deshacer. Utiliza :q para salir, :n para añadir un salto de línea, :c para borrar todo el contenido, :u para deshacer y :r para rehacer. Todo lo demás será agregado al texto y se mostrará en la consola.

using System;
using System.Text;
using System.Collections;

namespace Fermasmas.Wordpress.Com
{
  class Program
  {
    static Stack _undo;
    static Stack _redo;
    static string _text;
    
    static void Main(string[] args)
    {
      _undo = new Stack();
      _redo = new Stack();
      _text = string.Empty;

      string query = string.Empty;

      do
      {
        Console.Clear();
        Console.Write(_text);

        Console.WriteLine("\n\n\n\n");
        Console.WriteLine("Ingrese texto o :q para terminar.");
        Console.Write("> ");

        query = Console.ReadLine();
        if (query == ":n")
        {
          _undo.Push("\n");
          _redo.Clear();
          _text += "\n";
        }
        else if (query == ":c")
        {
          _undo.Clear();
          _redo.Clear();
          _text = string.Empty;
        }
        else if (query == ":u")
        {
          if (_undo.Count > 0)
          {
            string undo = _undo.Pop() as string;
            _redo.Push(undo);
            int index = _text.LastIndexOf(undo);
            _text = _text.Remove(index, undo.Length);
          }
        }
        else if (query == ":r")
        {
          if (_redo.Count > 0)
          {
            string redo = _redo.Pop() as string;
            _undo.Push(redo);
            _text += redo;
          }
        }
        else if (query == ":?")
        {
          Console.Clear();
          Console.WriteLine("Comandos");
          Console.WriteLine(":n - salto de línea");
          Console.WriteLine(":c - borrar");
          Console.WriteLine(":u - deshacer última acción");
          Console.WriteLine(":r - rehacer última acción deshecha");
          Console.WriteLine(":q - quitar");
          Console.WriteLine("Presione una tecla para continuar...");
          Console.ReadKey(true);
        }
        else if (query != ":q")
        {
          _undo.Push(query);
          _redo.Clear();
          _text += query;
        }
      }
      while (query != ":q");
    }
  }
}

Conclusiones

Bueno, pues eso es todo por ahora. Hay muchos más ejemplos, pero creo (espero) que estos son ilustrativos. Por supuesto, puedes reemplazar una pila y una cola con arrays o listas, pero bueno, tendrías que hacer las validaciones a manita.

Utiliza sabiamente las pilas y las colas, ya que el abusar de ellas también puede acarrear problemas de escalamiento o rendimiento de recursos.

Y ahora sí, a festejar que México le ganó a Francia…

Categorías:.NET Framework, C#, Tutorial Etiquetas: ,

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


Los arrays son estructuras de datos que nos permiten almacenar objetos de forma contigua. Los arrays tienen un tipo de dato bien definido. En C#, los declaramos utilizando corchetes, y los asignamos indicando el número de elementos que tendrán.

// inicialización explícita
int[] nums = new nums[5];

// inicialización implícita y asignación
int[] nums = new nums[] { 1, 2, 3, 4, 5 };

// inicialización y asignación implícita
int[] nums = { 1, 2, 3, 4, 5 };

Propiedades básicas

Los arrays tienen algunas propiedades interesantes. Length y LongLenth regresan el número de elementos que tiene un array. Por su parte, Rank regresa el número de dimensiones del array. Ojo, Length nos dice cuántos elementos existen, pero no nos dice cuántos elementos por dimensión hay. En el ejemplo anterior, Length nos regresa 15, mientras que Rank nos regresa 2. Cuando trabajamos con arrays unidimensionales (Rank == 1)  no hay problema, pero si trabajamos con más dimensiones podemos tener problemas. Para paliarlos, podemos hacer uso tanto de Rank como de dos métodos: GetLowerBound y GetUpperBound. Estos métodos regresan el índice mínimo y máximo, respectivamente, de una dimensión determinada. Por supuesto, GetLowerBound siempre regresará cero (o al menos no puedo pensar algún caso en el que el inicio de un array no sea cero). Así, podemos recorrer un array multidimensional iterando sobre el valor de Rank, y de ahí un bucle anidado que itere desde GetLowerBound hasta GetUpperBound.

Por otra parte, los arrays cuentan con dos propiedades: GetValue y SetValue. Estos métodos cuentan con sobrecargas para obtener o establecer los valores en las posiciones indicadas en los parámetros, y se puede utilizar incluso con varias dimensiones.

static void Main(string[] args)
{
  int[,] nums = new int[,] { 
    { 1, 2, 3, 4, 5 }, 
    { 6, 7, 8, 9, 10 }, 
    { 11, 12, 13, 14, 15 } 
  };

  int num = (int)nums.GetValue(1, 3);
  Console.WriteLine(num);

  nums.SetValue(0, 2, 4);
  Console.WriteLine(nums[2, 4]);

  Console.ReadKey(true);
}

El programa anterior escribe un “9” seguido de un “0” en la siguiente línea.

Nota que GetValue regresa un object, y SetValue utiliza un object como primer parámetro (lo cual es lógico porque el array puede ser de cualquier tipo de dato). Esto provoca que para arrays de tipos valor (estructuras, ValueType) habrá un boxing y unboxing esperándonos, efectívamente alentando nuestra aplicación. Así que es más eficiente que utilices los indexadores normalitos (corchetes) de los arrays.

Pero entonces, la pregunta… ¿como para qué proveer esos métodos? La respuesta es que todos los arrays derivan de una clase base, y que cuando tienes un método que tenga que trabajar con arrays sin importar el tipo de dato, se utiliza ésta para la referencia.

 

La clase base

Los arrays son objetos del CLR y como tal, ultimadamente heredan de object. En particular, existe una clase abstracta de la cual heredan todos los arrays: la clase Array, ubicada en el espacio de nombres System. La herencia la hace en automático el compilador, en modo ninja. Y uno, simple mortal, no puede heredar de dicha clase: de hacerlo, el compilador provocará un error de compilación (CS0644). Para usar los arrays debemos utilizar la construcción propia del lenguaje. Pero sí podemos referenciar cualquier array a través de esta clase.

Array nums = new int[] { 1, 2, 3, 4, 5 };

Incluso podemos referenciar arrays multidimensionales.

Array nums = new int[,] 
  { 
      { 1, 2, 3, 4, 5 }, 
      { 1, 2, 3, 4, 5 }, 
      { 1, 2, 3, 4, 5 } 
  };

Ahora sí tiene sentido utilizar las propiedades y métodos que describimos en la sección anterior.

La clase Array cuenta con algunos métodos estáticos que nos pueden servir mucho. En primera, tenemos CreateInstance, que nos permite crear un array en tiempo de ejecución dado su tipo de dato y número de dimensiones.

Array nums = Array.CreateInstance(typeof(int), 3, 5);
for (int i = 0; i < 3; i++)
  for (int j = 0; j < 5; j++)
    nums.SetValue((i * 5) + j + 1, i, j);

for (int i = 0; i < 3; i++)
  for (int j = 0; j < 5; j++)
    Console.WriteLine(nums.GetValue(i, j));

Podemos realizar búsquedas binarias con el método BinarySearch. Por supuesto, este algoritmo requiere que el array esté ordenado. No problemo, usamos el método Sort antes.

Random rnd = new Random();
Array nums = Array.CreateInstance(typeof(int), 10);
for (int i = 0; i < nums.Length; i++)
  nums.SetValue(rnd.Next(0, 10), i);

Array.Sort(nums);
for (int i = 0; i < nums.Length; i++)
  Console.WriteLine(nums.GetValue(i));

int index = Array.BinarySearch(nums, 4);
Console.WriteLine("Posición: {0}", index);

Array.Sort utiliza el comparador por defecto del tipo de dato. Podemos especificar algún comparador en particular, o incluso algún predicado. El siguiente ejemplo genera diez números aleatorios y los ordena de forma descendente, utilizando un delegado comparador.

Random rnd = new Random();
int[] nums = new int[10];
for (int i = 0; i < nums.Length; i++)
  nums[i] = rnd.Next(0, 10);

Array.Sort(nums, (n1, n2) => -n1.CompareTo(n2));
for (int i = 0; i < nums.Length; i++)
  Console.WriteLine(nums.GetValue(i));

Array.Foreach nos permite realizar una acción (pasada como delegado) a ejecutar sobre cada elemento del array. El siguiente ejemplo genera un array de 10 números aleatorios y los imprime en la consola, con un código más compacto.

Random rnd = new Random();
int[] nums = new int[10];
for (int i = 0; i < nums.Length; i++)
  nums[i] = rnd.Next(0, 10);

Array.ForEach(nums, n => Console.WriteLine(n));

 

Los métodos Find, FindAll y FindLast obtienen el primer elemento, todos, y el último elemento, respectivamente, que coincida con un predicado de búsqueda. En el siguiente ejemplo generamos un array de 10 números aleatorios y buscamos aquellos que sean pares.

Random rnd = new Random();
int[] nums = new int[10];
for (int i = 0; i < nums.Length; i++)
  nums[i] = rnd.Next(0, 10);

int[] pairs = Array.FindAll(nums, n => n == 0 || n % 2 == 0);
Array.ForEach(pairs, n => Console.WriteLine(n));

FindIndex y FindLastIndex funcionan igual que Find y FindLast, solo que en lugar de devolvernos un elemento del array nos devuelve el índice de dicho elemento.

Por su parte, el método TrueForAll ejecuta un predicado (un delegado de tipo Predicate<T>) sobre cada elemento del array. Éste le pasa el elemento al predicado, uno a uno, y el predicado devuelve verdadero o falso. TrueForAll devuelve verdadero en caso de que todos los predicados hayan devuelto verdadero. El siguiente ejemplo crea un array de diez números con valores aleatorios, y luego utiliza TrueForAll para determinar si todos los elementos del array son menores e iguales a cinco o no.

Random rnd = new Random();
int[] nums = new int[10];
for (int i = 0; i < nums.Length; i++)
  nums[i] = rnd.Next(0, 10);

Array.ForEach(nums, n => Console.Write("{0} ", n));
Console.WriteLine();

bool value = Array.TrueForAll(nums, n => n <= 5);
Console.WriteLine("¿Son todos menores a cinco? {0}", value);

Otro método interesante es ConvertAll. Éste método toma como parámetro algún array de tipo especificado y lo convierte en algún otro array, a través de un delegado, el cual realiza la conversión de un elemento en otro. Por ejemplo, el siguiente código muestra cómo convertir y transformar un array de números aleatorios en un array de cadenas de texto.

Random rnd = new Random();
int[] nums = new int[10];
for (int i = 0; i < nums.Length; i++)
  nums[i] = rnd.Next(0, 10);

Array.ForEach(nums, n => Console.Write("{0} ", n));
Console.WriteLine();

string[] strs = Array.ConvertAll<int, string>(nums, n => "Número " + n);
Array.ForEach(strs, str => Console.WriteLine(str));

 

Arrays como colecciones

Todas estas propiedades son útiles para manipular arrays. Pero este tutorial es sobre colecciones, no sobre arrays. ¿Qué tiene que ver? Bueno, un array implementa la interfaz ICollection (y por ende, IEnumerable), efectívamente haciéndola una colección. De hecho, Array también implementa IList, así que un array es una lista también. Veamos.

Si el array implementa IEnumerable, quiere decir que podemos iterar sobre los elementos del array a través de un foreach. En efecto.

Array nums = new int[] { 1, 2, 3, 4, 5 };
foreach (int num in nums)
  Console.WriteLine(num);

El iterador (y por ende, el foreach) regresa todos los elementos del array. Así, en un array multidimensional (i.e. Rank > 1) se regresan los elementos de la primera dimensión más los de la segunda, y así sucesivamente.

int[,] nums = new int[,] 
  { 
    { 1, 2, 3, 4, 5 }, 
    { 6, 7, 8, 9, 10 }, 
    { 11, 12, 13, 14, 15 } 
  };
foreach (int num in nums)
  Console.WriteLine(num);

El ejemplo anterior imprime los números del 1 al 15.

Por otra parte, el implementar ICollection me garantiza al menos una propiedad Count para saber el número de elemetos que contiene, más una propiedad SyncRoot que me permita bloquear el acceso al array usando la cláusula lock. En efecto, SyncRoot está disponible en cualquier array, pero Count se implementa expícitamente, así que para acceder a ésta hay que hacer una conversión (explícita o implícita) hacia la interfaz ICollection para poder acceder a Count.

int[] nums = new int[] { 1, 2, 3, 4, 5 };
      
ICollection col = nums; // conversión implícita
lock (col.SyncRoot)
{
  Console.WriteLine("Count: {0}", col.Count);
  foreach (object num in col)
    Console.WriteLine(num);
}

Escapa a mi entendimiento el por qué el equipo de .NET definió la propiedad Length cuando ya tenía Count, y en cambió decidió ocultar ésta última. En fin, el chiste es que Count, como bien podemos ver utilizando Reflector y desensamblando mscorlib.dll, regresa lo mismo que Length. Pero bueno, así es la vida…

Más interesante es el hecho de que implementen IList. Por supuesto, si hacemos la conversión a IList de un array, y de ahí llamamos a Add, Remove, RemoveAt o Insert, obtendremos una NotSupportedException. Si llamamos al método Clear, empero, lo que pasará es que el array se inicializará de nuevo. Es decir, si el array es de tipo valor, se establecerá a cero y todos sus miembros se establecerán a cero o null. Si el array es de tipo referencia, los miembros se establecerán a null.

El siguiente ejemplo ilustra cómo se lanzan las excepciones esperadas. A su vez, se utiliza Clear para re-establecer todo el array.

int[] nums = new int[] { 1, 2, 3, 4, 5 };
Array.ForEach(nums, n => Console.WriteLine(n));

IList list = nums; // conversión implícita
try { list.Add(6); }
catch { Console.WriteLine("Error al agregar."); }
try { list.Remove(1); }
catch { Console.WriteLine("Error al remover."); }
try { list.Insert(0, 6); }
catch { Console.WriteLine("Error al insertar."); }

list.Clear();
Array.ForEach(nums, n => Console.WriteLine(n));

La salida en consola es la siguiente:

1
2
3
4
5
Error al agregar.
Error al remover.
Error al insertar.
0
0
0
0
0

Otro hecho interesante es el indexador de IList. Sabemos que un array tiene su indexador. Pues bien, el indexador de IList regresa los mismos elementos que el indexador del array. Pero ojo, que esto solo funciona para arrays unidimensionales. Si intentas acceder al indexador de un array multidimensional, tendrás un ArgumentException. El siguiente ejemplo ilustra lo anterior.

int[] nums = new int[] { 1, 2, 3, 4, 5 };
IList list = nums; // conversión implícita
for (int i = 0; i < list.Count; i++)
{
  object obj = list[i];
  Console.Write("{0} ", obj);
}
Console.WriteLine();

int[,] multinums = new int[,] { { 1, 2 }, { 1, 2 } };
list = multinums;
try
{
  for (int i = 0; i < list.Count; i++)
  {
    object obj = list[i];
    Console.Write("{0} ", obj);
  }
}
catch (ArgumentException e)
{
  Console.WriteLine(e.Message);
}

La salida en consola es la siguiente.

1 2 3 4 5
Array was not a one-dimensional array.

Con respecto a IList.IndexOf e IList.Contains, ambas declaradas explícitamente en la clase Array, cabe decir que IndexOf hace uso directamente de Array.IndexOf, y Contains regresa verdadero si el valor devuelto por IndexOf es mayor o igual al devuelto por GetLowerBound. Sobra decir, por cierto, que las propiedades IsReadOnly e IsFixedSize son false y true, respectivamente.

Los arrays son muy buenos para guardar conjunto de objetos no variables… pero… ¿qué pasa con

Los arrays son mucho muy útiles, pero tienen un defecto: no están hechos para que cambien de tamaño. Por supuesto, siempre podemos utilizar Array.Resize, pero esto tiende a ser ineficiente, sobre todo si lo usamos a cada oportunidad. Hay, sin embargo, una mejor opción: ArrayList.

Clase ArrayList

La clase ArrayList, ubicada en System.Collections, es una de las listas más utilizadas, dada su versatilidad. Y no es de extrañarse, dado que está desde .NET 1.0. ArrayList implementa IList, ICollection e IEnumerable, además de ICloneable. Es también una de las colecciones más versátiles que hay. Veamos cómo utilizarla, aunque he de decir que es muy intuitiva.

En primer lugar, cuenta con tres constructores: uno por default, uno que toma una colección cualquiera (ICollection) y agrega sus elementos a esta colección, y una que determina la capacidad inicial.


// inicializa un ArrayList dada una colección cualquiera
string[] strs = {"hugo", "paco", "luis" };
ArrayList list = new ArrayList(strs);

El método Add nos permite agregar un elemento mientras que AddRange nos permite agregar varios (toma como parámetro un ICollection). En contraparte, Remove, RemoveAt y RemoveRange  nos permite eliminar elementos dado un objeto en particular a eliminar, un índice y un índice con su rango.


ArrayList list = new ArrayList();

list.Add("donald");
list.Add("mcpato");

string[] strs = {"hugo", "paco", "luis" };
list.AddRange(strs);

list.Remove("donald");
list.RemoveAt(0); // remueve "mcpato"
list.RemoveRange(0, 2); // remueve "hugo" y "paco"

Dado que ArrayList implementa IList, tenemos las propiedades IsFixedSize, IsReadOnly, Count y SyncRoot, además del conocido indexador. Además, implementa Clear para borrar todo el contenido de la lista, y Contains para determinar si un elemento existe en la colección o no.

IndexOf nos permite obtener el índice de algún objeto (o –1 si éste no existe). CopyTo nos permite copiar el contenido a un array, mientras que ToArray devuelve un array con los objetos de la colección.

Por su parte, BinarySearch realiza una búsqueda binaria y nos devuelve el índice donde se encuentra. Para hacer una búsqueda binaria la colección tiene que estar ordenada, por lo que podemos usar el método Sort. El método Sort por default obtiene el IComparer por defecto del objeto en cuestión, o bien se le puede especificar IComparer de forma predefinida.


ArrayList list = new ArrayList();

list.Add("donald");
list.Add("mcpato");

string[] strs = {"hugo", "paco", "luis" };
list.AddRange(strs);

list.Sort(); // utiliza la comparación estándar de texto
int index = list.BinarySearch("hugo");
if (index >= 0)
   Console.WriteLine(list[index]);

La clase también cuenta con algunos métodos estáticos interesantes. Por ejemplo, ArrayList.Adapter convierte un IList en un ArrayList. FixedSize convierte un ArrayList en una lista de solo lectura y FixedSize devuelve un ArrayList que imposibilita el agregar o eliminar elementos.

Tristemente ArrayList acepta un objet, así que podemos agregar cualquier tipo de objeto, incluso mezclar varios tipos. Esto nos provoca problemas si tratamos con tipos valor (ValueType), debido al boxing y unboxing. Aún así, es común encontrar colecciones que derivan de ArrayList.

class StringList : ArrayList
{
    public StringList()
        : base()
    {
    }

    public void Add(string str)
    {
        base.Add(str);
    }

    public string this[int index]
    {
        get { return base[index] as string; }
    }
}

ArrayList no es, sin embargo, la mejor base para crear nuestras propias colecciones. En efecto, existen CollectionBase y DictionaryBase para ello, pero éstas son tema de otra entrada en el blog. Por otra parte, ArrayList ha perdido popularidad a partir de que salieron los genéricos en .NET 2.0 y se creó la clase List<T>, que es más eficiente que ArrayList para tipos valor (ValueType) ya que no incurren en las penas de boxing y unboxing. Sin embargo, ArrayList puede ser una solución sencilla cuando necesitamos arrays dinámicos con funcionalidad básica.

Conclusiones

Hemos visto cómo los arrays son otro tipo de colecciones, aunque éstos no sean dinámicos. Y cómo ArrayList funciona como un array dinámico. Pero todavía queda mucho camino por recorrer en esto de las colecciones.

En la siguiente entrada veremos algunos tipos especiales de colecciones: las pilas y las colas. Por ahora, es tiempo de ir a dormir.

Categorías:.NET Framework, C#, Tutorial Etiquetas: , ,

Todo lo que siempre quisiste saber sobre colecciones y tenías miedo de preguntar… Colecciones, listas y diccionarios


En la entrada anterior comentábamos sobre cómo las enumeraciones (IEnumerable, IEnumerator) nos daban la posibilidad de listar un conjunto de objetos determinados. Sin embargo, a pesar de esto, las enumeraciones (también llamadas iteradores) no son colecciones en sí mismas.

Habíamos definido una enumeración como un simple conjunto de objetos cuyo contenido podía accederse a través del patrón conocido como iterador. Una colección es, pues, un conjunto de objetos enumerable con un número definido de objetos y que expone una forma determinada para acceder a éstos (aparte del iterador). La clase base para cualquier colección se representa por la interfaz ICollection.

ICollection (que deriva de IEnumerable) define una propiedad Count, que nos permite saber con cuántos elementos cuenta la colección, y provee un mecanismo para poder sincronizar el acceso a los elementos para cuando trabajamos con múltiples hilos, a través de la propiedad SyncRoot. Sin embargo, vemos que ICollection no define por sí misma una forma para agregar o eliminar objetos, y fuera de las dos propiedades anteriores y el método CopyTo (que nos permite copiar el contenido de la colección a un array), la interfaz luce muy similar a IEnumerable.

La razón de esto es que cada clase derivada define cómo se debe acceder a los métodos. Básicamente, .NET distingue entre dos formas básicas de colecciones: las listas y los diccionarios.

Listas

Una lista es una colección que guarda objetos cuya principal característica es que éstos se encuentran accesibles a través de un índice. Por ejemplo, si tengo la colección de nombres { “Fernando”, “Catia”, “Moisés” } puedo decirle a mi lista que me devuelva un elemento a través de un índice, de tal suerte que si le digo que me devuelva el segundo elemento, la lista me devolverá “Catia”.

Antes de continuar, me gustaría hacer notar un punto importante. Si tienes experiencia programando en algún otro lenguaje como C++, seguramente estás familiarizado con el concepto de estructura de dato, muy en particular, con el de lista. Tradicionalmente, se denomina lista a una estructura de datos que me permite acceder a su primer elemento, el cual está enlazado con un siguiente objeto, y éste a su vez está enlazado con el tercer objeto y el primero. Es decir, los objetos están enlazados entre sí, y por la naturaleza misma yo no puedo acceder a algún elemento dado un índice de forma eficiente, ya que para hacerlo tendría que iterar sobre el primer elemento, obtener su enlace al segundo, con éste obtener el enlace al tercer elemento y así hasta llegar al n-ésimo elemento, lo cual es terriblemente ineficiente. Sin embargo, el concepto de lista en .NET es muy diferente al concepto tradicional, como hemos visto. Esto es así no porque haya una diferencia de concepto, sino porque el equipo que diseñó .NET simplemente decidió nombrar “lista” a algo que evidentemente no es una lista en el sentido tradicional. Esto es importante, ya que no hay que confundir términos y de hecho .NET provee ciertas clases para trabajar con listas enlazadas (que desafortunadamente también llevan el nombre “lista”, como LinkedList, aumentando la confusión en cuanto al nombre). Para bien o para mal, lo hecho hecho está, así que nos quedamos con el nombre “lista” para las colecciones que pueden accederse a través de un índice y cuyos elementos no necesariamente están enlazados.

Dicho lo anterior, regresemos a donde estábamos. La interfaz base para toda lista (recuerda el párrafo anterior) es IList. Por supuesto, ésta hereda de ICollection, y por ende, de IEnumerable.

La interfaz IList define dos propiedades importantes: IsFixedSize e IsReadOnly. La primera propiedad nos indica si la lista en cuestión permite agregar y eliminar objetos de la colección o no, mientras que la segunda nos indica si la colección puede ser alterada, ya sea agregando y eliminando objetos, o bien modificando los ya existentes (por ejemplo, dada una lista con tres elementos, reemplazar el segundo por algún otro). Con respecto a los métodos, tenemos que IList cuenta con dos que nos permite saber si un elemento pertenece a la colección o no. El método Contains toma como parámetro un objeto y nos devuelve verdadero si éste existe, o falso si no lo hace. Por su parte, el método IndexOf toma como parámetro un objeto y nos regresa un número que representa la posición (o el índice) de éste dentro de la colección, o –1 si el objeto no pertenece a la lista. Por otra parte, IList define algunos métodos para modificar la colección: Add nos permite agregar elementos, mientras que Remove los elimina de la colección. Alternativamente, RemoveAt elimina un elemento por su índice. Finalmente, Clear remueve todos los objetos de la colección. Por último, IList define un indexador a través del cual podemos obtener o establecer un elemento dado su índice (de la forma object o = list[0], o list[0] = new object(); ).

Hay que tener cuidado sobre cómo manejamos nuestra lista. Por ejemplo, el método RemoveAt usualmente lanza una excepción IndexOfOutRangeException si el índice es menor a cero o mayor o igual a lo especificado por la propiedad Count. Por su parte, la mayoría de las clases que implementan esta interfaz lanzarán una excepción si se intenta llamar a Add, Remove, RemoveAt o Clear cuando la propiedad IsFixedSize es verdadera. Adicionalmente, si la propiedad IsReadOnly es verdadera, usualmente se lanzará una excepción si se intenta cambiar algún elemento a través del indexador.

El ejemplo más sencillo de una lista es un array. La clase base para todos los arrays es Array, definida en el espacio de nombres System, la cual implementa IList. Ésta es una clase abstracta, cuyas clases concretas las crea el compilador de C# cuando declaramos un array normal. En otra entrada hablaremos más a fondo de los arrays, por el momento basta decir que un array es una lista. Así, consideremos el siguiente ejemplo.

int[] nums = new int[] { 5, 4, 3, 2, 1 };
IList list = nums;

Console.WriteLine("IsReadOnly: {0}", list.IsReadOnly);
Console.WriteLine("IsFixedSize: {0}", list.IsFixedSize);
Console.WriteLine("Contiene al 3? {0}", list.Contains(3));
Console.WriteLine("Índice del 7: {0}", list.IndexOf(7));
for (int i = 0; i < 5; i++)
  Console.WriteLine("Índice {0} Valor {1}", i, list[i]);
foreach (int num in nums)
  Console.WriteLine("Enumerando {0}", num);

try {
  list.Add(6);
} catch (NotSupportedException e) {
  Console.WriteLine("Error al agregar elemento:\n\t{0}", e.Message);
}

Console.ReadKey(true);

Éste genera la siguiente salida:

IsReadOnly: False
IsFixedSize: True
Contiene al 3? True
Índice del 7: -1
Índice 0 Valor 5
Índice 1 Valor 4
Índice 2 Valor 3
Índice 3 Valor 2
Índice 4 Valor 1
Enumerando 5
Enumerando 4
Enumerando 3
Enumerando 2
Enumerando 1
Error al agregar elemento: 
    Collection was of a fixed size.

Diccionarios

 

Un diccionario, por otro lado, es una colección de pares llave-valor, cuya característica principal es que un objeto es accesible a través de su llave. Por ejemplo, si tengo un diccionario donde la llave es un nombre y el valor la edad, podría tener algo como { “Fernando” => 27, “Catia” => 29, “Moisés” => 30 }. A diferencia de una lista, no puedo decirle que me regrese el segundo elemento, pero sí puedo decirle que me de el valor asociado a “Catia”, en cuyo caso el diccionario me regresaría el número 29.

La interfaz que define a un diccionario es IDictionary, que hereda de ICollection y por ende de IEnumerable. Al igual que IList, tiene propiedades IsReadOnly e IsFixedSize que nos indican si el diccionario puede modificarse y alterar su tamaño, respectivamente. También cuenta con un método Add y un método Remove para agregar y eliminar elementos, con la salvedad que Add toma dos parámetros: la llave y el valor, y Remove toma un parámetro: la llave. Clear elimina todo el contenido y Contains devuelve verdadero si la llave (pasada como parámetro) existe o no dentro de la colección. Adicionalmente, la interfaz cuenta con un indexador, pero éste toma como parámetro una llave y devuelve el valor, o bien permite modificar el valor que corresponda con la llave dada.

IDictionary también cuenta con dos propiedades importantes: Keys y Values. Ambas regresan un ICollection, y representa el conjunto de llaves y el conjunto de valores, por si deseamos leerlos de forma independiente.

Finalmente, llama la atención un método: GetEnumerator, que a diferencia del que provee IEnumerable, éste devuelve un objeto de tipo IDictionaryEnumerator. A diferencia de IEnumerator, que enumera objetos cualesquiera, IDictionaryEnumerator enumera objetos del tipo DictionaryEntry. Esta clase tiene dos propiedades: Key y Value. Por supuesto, la finalidad es que podamos obtener la relación llave-valor en un solo objeto. Esto es sumamente importante, dado que cuando iteramos sobre un diccionario, el tipo de dato devuelto no será el de la llave o el del valor, sino que será del tipo DictionaryEntry. Así, para hacer un foreach con un diccionario haríamos algo como:

IDictionary dic = ...;
foreach (DictionaryEntry entry in dic)
  Console.WriteLine("Llave: {0}, Valor: {1}", entry.Key, entry.Value);

Una alternativa a lo anterior sería iterar sobre las llaves (propiedad Keys) y con eso obtener el valor asociado.

IDictionary dic = ...;
foreach (object key in dic.Keys)
{
  object value = dic[key];
  ...
}

Por supuesto, lo anterior es más ineficiente porque tiene que hacer dos lecturas en lugar de una. Pero es una buena alternativa de todas formas.

Si analizamos bien lo que se ha dicho sobre diccionarios, podríamos decir que éstos no son otra cosa que un array de objetos tipo DictionaryEntry. De hecho, podríamos decir que la diferencia primordial entre un IList y un IDictionary es que IList me permite acceder a la colección mediante índice, mientras que el IDictionary mediante una llave cualquiera.

IDictionary no cuenta con una estructura sencilla como IList con el array. Pero podemos decir que el diccionario básico es la clase Hashtable. Por supuesto, ésta clase merece su propia entrada y será tratada en este tutorial, a su debido tiempo.

Cómo implementar nuestras propias colecciones

 

Rara vez se dará el caso en el que tengas que implementar tu propia clase que derive de IList e IDictionary. Y de hecho, la mayoría de las veces que tengas que hacer esto derivarás de alguna colección existente, o bien utilizarás arrays o clases existentes para ocultar el funcionamiento. Aún así, hay algunas recomendaciones que se pueden seguir.

Utiliza la interfaz apropiada. Ya hemos explicado IList e IDictionary, así que cuando quieras una lista o un diccionario, ya sabes. Solo si ninguna de estas dos se ajusta a lo que quieres, deriva de ICollection.

Las clases Stack y Queue son un ejemplo clásico de esto. Dado que ambas son listas pero no permiten acceso a través de indexador ni aleatorio, no pueden derivar de IList. Y ciertamente no son diccionarios. Así, lo que hacen es derivar de ICollection e implementar sus propios métodos para agregar y eliminar elementos (como Push, Pop, Peek, Enqueue y Dequeue).

Oculta métodos que no soportes. Por ejemplo, si tienes que implementar IList porque necesitas que la colección permita agregar elementos, pero no eliminarlos, entonces implementa el método IList.Remove de forma explícita.

class MyList : IList
{
   public void Add(object obj) { ... }

   IList.Remove(object obj) { ... }
}

En el ejemplo anterior, implementar IList.Remove de forma explícita hará que el método no esté disponible dada una instancia de MyList. Para acceder a IList.Remove, tendría que hacerse una conversión explícita de MyList a IList, de tal suerte que una llamada directa desde MyList provocará un error de compilación. En Visual Studio, el intellisense ni siquiera muestra el método.

Utiliza sabiamente NotSupportedException. Implementar explícitamente una interfaz no previene que el método no sea llamado. En el ejemplo expuesto, hacer una conversión explícita a IList desocultará el método en cuestión.

object obj = new object();

MyList list = new MyList();
list.Add(obj);
// list.Remove(obj); provocaría error de compilación

IList explicitList = (IList)list;
explicitList.Remove(obj); // esto sí compila

En el ejemplo, Remove nunca debería ser llamada. Por ello, si alguien quiere pasarse de listo, debe recibir su castigo, lanzando una excepción. En estos casos, lo indicado es lanzar una NotSupportedException. Ejemplo:

class MyList : IList
{
   public void Add(object obj) { ... }

   IList.Remove(object obj) { 
      throw new NotSupportedException("Método Remove no puede ser llamado. ");
   }
}

No olvides documentar esto en tu código para que quien lo utilice sepa a qué se atiene y pueda evitar problemas y bugs.

Presta atención al tema de la sincronización. En ocasiones nos olvidamos del tema de la concurrencia y si no prestamos atención, boom, problemas de sincronización cuando trabajamos con múltiples hilos.

ICollection ofrece la propiedad SyncRoot, la cual cuando la implementemos será una simple instancia de object. Pero es importante ya que nos permitirá utilizar la cláusula lock para asegurarnos que ningún otro hilo podrá acceder a nuestra colección mientras trabajamos. Por ejemplo:

class MyList : IList
{
   ...

   public void WriteAll()
   {
      for (int i = 0; i < Count; i++)
         Console.WriteLine(this[i].ToString());
   }
}

Si ejecutamos WriteAll y mientras ejecutamos el bucle for algún otro hilo inserta un nuevo elemento, nuestro método no imprimiría todos los que ha encontrado. Esto se resuelve con un lock.

class MyList : IList
{
   private object _syncRoot = new object();
   ...

   public object SyncRoot
   {
      get { return _syncRoot; }
   }

   public void WriteAll()
   {
      lock (SyncRoot)
      {
         for (int i = 0; i < Count; i++)
            Console.WriteLine(this[i].ToString());
      }
   }
}

Y todos somos felices.

Valida los datos de la colección. Las interfaces descritas al momento se basan en el hecho de que se puede agregar cualquier tipo de dato a una colección, y por ello los métodos piden parámetros de tipo object. Así, a una implementación de IList cualquiera yo le puedo agregar cadenas de texto, números reales o valores booleanos, indistintamente. Lo cual no sería correcto si estoy tratando con objetos de tipo char. Por ello es que siempre debemos validar que nuestra colección sea consistente, para evitar posibles errores.

Por ejemplo, si implemento un IList que solo acepta números enteros, tengo que poner esa validación en el método Add (además de Contains y Remove, pero éstos son opcionales). Lo tradicional es lanzar una excepción de tipo ArgumentException.

class MyList : IList
{
   public void Add(object obj)
   {
      if (obj == null)
         throw new ArgumentNullException("obj");
      if (! (obj is Int32))
         throw new ArgumentException("Solo se aceptan enteros. ");

      ... etc ...
   }
}

 

Problemas conocidos

 

Quizás el problema más grave de estas interfaces es el hecho de que todas se basan en un tipo de dato object. Para cualquier clase, no hay problema, solo hay que hacer la conversión correspondiente con un simple cast. El problema es cuando tratamos con estructuras.

En efecto, cuando tenemos un objeto tipo valor (estructura, es decir, que hereda de ValueType) y la guardamos en una variable de tipo object, entonces ocurre un proceso conocido como boxing y unboxing, que lo que hace es crear otra variable que “envuelva” el contenido de la estructura en cuestión. Hacer esto es costosísimo en términos de recursos. Si agregamos 1000 enteros a un IList cuyo método Add recibe un object, se harán 1000 boxings y posiblemente 1000 unboxings, lo cual alentará mucho tu programa.

La solución a estos casos es utilizar las interfaces genéricas, como ICollection<T>, IList<T> e IDictionary<TKey, TValue>, definidas en System.Collection.Generics. Pero eso es tema de otra entrada en este tutorial.

Y para terminar…

 

Ya para terminar y poder irme a dormir, unos comentarios finales. Esta entrada en realidad es informativa. Me interesa exponer los principios básicos sobre los cuales se basan las colecciones en .NET. En realidad rara vez tendrás que implementar estas interfaces, y ciertamente es mejor heredar de algunas como CollectionBase y DictionaryBase, creadas para ese propósito, o algunas más avanzadas como Dictionary<K, V> y Collection<T>. Pero eso lo veremos más adelante. Lo importante aquí es captar la esencia de las colecciones y sobre todo aprender las diferencias entre listas y diccionarios, así como sus conceptos básicos.

Realmente esta entrada es aburrida porque es pura teoría. Pero a partir de ahora, las cosas comenzarán a mejorar, lo prometo. Sigue sintonizando el tutorial, que en la siguiente oportunidad hablaremos de los arrays.

Tschüss!

Categorías:.NET Framework, C#, Tutorial Etiquetas: , ,