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

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!

Anuncios
Categorías:.NET Framework, C#, Tutorial Etiquetas: , ,
  1. ozkar
    diciembre 7, 2012 en 9:55 am

    Mejor explicado que en la pagina de msdn gracias

  1. noviembre 23, 2010 en 2:04 pm

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